From 4e04d4d83147097610071591f5eaeaadd3c16de3 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sun, 1 Mar 2015 22:33:26 +1000 Subject: [PATCH 01/49] added timezone picker for users and sites --- .gitignore | 4 -- Gemfile | 5 ++ Gemfile.lock | 4 ++ app/assets/javascripts/application.js | 2 + app/models/site.rb | 4 ++ app/models/user.rb | 4 ++ app/views/devise/registrations/edit.html.haml | 1 + app/views/layouts/application.html.haml | 2 +- app/views/{ => shared}/_navbar.html.haml | 0 .../shared/_time_zone_select_custom.html.haml | 48 +++++++++++++++++++ app/views/sites/_form.html.haml | 8 ++-- config/initializers/assets.rb | 2 +- lib/modules/time_zone_helper.rb | 46 ++++++++++++++++++ .../moment-timezone-with-data.min.js | 7 +++ .../javascripts/moment-with-locales.min.js | 10 ++++ 15 files changed, 138 insertions(+), 9 deletions(-) rename app/views/{ => shared}/_navbar.html.haml (100%) create mode 100644 app/views/shared/_time_zone_select_custom.html.haml create mode 100644 lib/modules/time_zone_helper.rb create mode 100755 vendor/assets/javascripts/moment-timezone-with-data.min.js create mode 100755 vendor/assets/javascripts/moment-with-locales.min.js diff --git a/.gitignore b/.gitignore index b19a4cfc..640d4f95 100644 --- a/.gitignore +++ b/.gitignore @@ -37,10 +37,6 @@ external_modules.log # ignore test coverage files /coverage/ -# ignore vendor binary files -/vendor/bin/* -/vendor - # ignore generated api documentation /doc /.yardoc/ \ No newline at end of file diff --git a/Gemfile b/Gemfile index 15cf554f..1a591a91 100644 --- a/Gemfile +++ b/Gemfile @@ -60,6 +60,7 @@ gem 'gmaps4rails', '< 2' # git grep --full-name --name-only '.fa-play' $(git rev-list --all) > font-awesome-find.txt # Don't update this, as site still uses bootstrap v2. Need to update this when bootstrap is updated. +# https://github.com/seyhunak/twitter-bootstrap-rails/tree/38476dbd7f9a99179388bffb101826d844029949 gem 'twitter-bootstrap-rails', git: 'https://github.com/seyhunak/twitter-bootstrap-rails.git', branch: :master, ref: '38476dbd7f' gem 'bootstrap-timepicker-rails', '~> 0.1.3' @@ -69,6 +70,10 @@ gem 'will_paginate', '~> 3.0.7' gem 'dotiw', git: 'https://github.com/radar/dotiw.git', branch: :master, ref: '89d936adc6' gem 'recaptcha', '~> 0.3.6', require: 'recaptcha/rails' +# for proper timezone support +gem 'tzinfo', '~> 1.2.2' +gem 'tzinfo-data', '~> 1.2015.1' + # USERS & PERMISSIONS # ------------------------------------- # https://github.com/plataformatec/devise/blob/master/CHANGELOG.md diff --git a/Gemfile.lock b/Gemfile.lock index d7cd84f0..9b4e6ca3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -448,6 +448,8 @@ GEM tins (1.3.4) tzinfo (1.2.2) thread_safe (~> 0.1) + tzinfo-data (1.2015.1) + tzinfo (>= 1.0.0) uglifier (2.7.1) execjs (>= 0.3.0) json (>= 1.8.0) @@ -542,6 +544,8 @@ DEPENDENCIES therubyracer (~> 0.12.1) thin (~> 1.6.3) twitter-bootstrap-rails! + tzinfo (~> 1.2.2) + tzinfo-data (~> 1.2015.1) uglifier (>= 1.3.0) uuidtools (~> 2.1.5) web-console (~> 2.0.0) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 269a88ae..9c3d2402 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -18,6 +18,8 @@ //= require bootstrap-datepicker //= require gmaps4rails/gmaps4rails.base //= require gmaps4rails/gmaps4rails.googlemaps +//= require moment-with-locales.min +//= require moment-timezone-with-data.min //= require bootstrap //= require shared //= require public diff --git a/app/models/site.rb b/app/models/site.rb index c312ed54..b040e6e0 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -119,6 +119,10 @@ def self.add_location_jitter(value, min, max) modified_value end + def tzinfo_tz + 'Australia - Brisbane' + end + # Define filter api settings def self.filter_settings { diff --git a/app/models/user.rb b/app/models/user.rb index 9cac916e..92cb55f6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -336,6 +336,10 @@ def get_membership_duration Time.zone.now - self.created_at end + def tzinfo_tz + 'Australia - Brisbane' + end + def ensure_authentication_token if authentication_token.blank? self.authentication_token = generate_authentication_token diff --git a/app/views/devise/registrations/edit.html.haml b/app/views/devise/registrations/edit.html.haml index b16f2fac..e4bfa3e3 100644 --- a/app/views/devise/registrations/edit.html.haml +++ b/app/views/devise/registrations/edit.html.haml @@ -12,6 +12,7 @@ .form-inputs = f.input :user_name, required: true, autofocus: true = f.input :email, required: true + = render partial: '/shared/time_zone_select_custom', locals: { f: f, attribute_name: :tzinfo_tz, model_name: resource_name } - if devise_mapping.confirmable? && resource.pending_reconfirmation? %p Currently waiting confirmation for: #{resource.unconfirmed_email} diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 6219258a..54e50e0f 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -19,7 +19,7 @@ = javascript_include_tag 'application' %body - = render partial: '/navbar' + = render partial: '/shared/navbar' .container diff --git a/app/views/_navbar.html.haml b/app/views/shared/_navbar.html.haml similarity index 100% rename from app/views/_navbar.html.haml rename to app/views/shared/_navbar.html.haml diff --git a/app/views/shared/_time_zone_select_custom.html.haml b/app/views/shared/_time_zone_select_custom.html.haml new file mode 100644 index 00000000..be280141 --- /dev/null +++ b/app/views/shared/_time_zone_select_custom.html.haml @@ -0,0 +1,48 @@ +- control_id = "#{model_name}_#{attribute_name}" +- span_id = "#{control_id}_appended_info" +- help_id = "#{control_id}_appended_help" +.control-group.string.optional{class: control_id } + = f.label attribute_name, 'Time Zone', class: 'string optional control-label', for: control_id + .controls + .input-append + = f.input_field attribute_name, type: 'text', autocomplete: 'off', class: 'span12' + %span.add-on{id: span_id, title: 'Time zone abbreviation and current UTC offset'} + (no match) + %p.help-block{id: help_id } + +:javascript + var mapping = #{TimeZoneHelper.mapping_zone_to_offset.to_json} + var mapping_keys = Object.keys(mapping); + var mapping_values = Object.keys(mapping).map(function (key) { return mapping[key]; }); + + $('##{control_id}').typeahead({ + 'source': Object.keys(mapping), + 'minLength': 2, + 'items': 10 + }); + + $('##{control_id}').on('change keydown keypress', function(){ + checkTimeZone(); + }); + + function checkTimeZone(){ + var enteredValue = $('##{control_id}').val(); + var mappingValue = mapping[enteredValue]; + + var appendedInfo = $('##{span_id}'); + var helpInfo = $('##{help_id}'); + if(mappingValue){ + appendedInfo.text(mappingValue); + var origTime = moment().tz(enteredValue.replace(' - ', '/')); + helpInfo.text('Currently: '+origTime.format("dddd, MMMM Do YYYY, h:mm:ss a z Z")); + } else { + appendedInfo.text('(no match)'); + helpInfo.text('(no match)'); + } + } + + // check time zone when page loads + $(document).ready(function() { + checkTimeZone(); + }); + diff --git a/app/views/sites/_form.html.haml b/app/views/sites/_form.html.haml index e016a632..667af926 100644 --- a/app/views/sites/_form.html.haml +++ b/app/views/sites/_form.html.haml @@ -8,12 +8,14 @@ = f.input :description, input_html: {rows: '6', class: 'span12' } = f.input :latitude, input_html: { class: 'span12', max: 90, min: -90 } = f.input :longitude, input_html: { class: 'span12', max: 180, min: -180 } - .control_group + .control_group.file.optional{class: :site_image} = f.label :image .controls - unless @site.image_file_name.blank? - = image_tag @site.image.url(:span1) - = f.file_field :image + = image_tag @site.image.url, class: 'file optional span4' + = f.file_field :image, class: 'span8' + .clearfix + = render partial: '/shared/time_zone_select_custom', locals: { f: f, attribute_name: :tzinfo_tz, model_name: :site } .span6= gmaps(markers: {data: @markers, options: { draggable: true } }, map_options: gmaps_default_options ) .row-fluid diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 54fc86d5..c7bd7ebd 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -4,7 +4,7 @@ Rails.application.config.assets.version = '1.0' # Add additional assets to the asset load path -# Rails.application.config.assets.paths << Emoji.images_path +Rails.application.config.assets.paths << Rails.root.join('vendor', 'assets').to_path # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. diff --git a/lib/modules/time_zone_helper.rb b/lib/modules/time_zone_helper.rb new file mode 100644 index 00000000..9f82af3c --- /dev/null +++ b/lib/modules/time_zone_helper.rb @@ -0,0 +1,46 @@ +require 'tzinfo' +require 'active_support' + +class TimeZoneHelper + class << self + def mapping_zone_to_offset + Hash[ + TZInfo::Timezone.all.map do |tz| + this_tz = TZInfo::Timezone.get(tz.identifier) + period = this_tz.current_period + abbr = period.abbreviation + offset = period.utc_total_offset # in seconds + offset_hours = offset / (60 * 60) + offset_minutes = (offset % 60).to_s.rjust(2, '0') + + if offset_hours < 0 + offset_hours = (offset_hours * -1).to_s.rjust(2, '0') + offset_hours = '-' + offset_hours + else + offset_hours = '+'+ offset_hours.to_s.rjust(2, '0') + end + + [ + tz.to_s, + "#{abbr} (#{offset_hours}:#{offset_minutes})" + ] + end + ] + end + + # Get the TZInfo Timezone equivalent to the Ruby TimeZone. + # @param [string] ruby_tz_name + # @return [TZInfo::Timezone] TZInfo Timezone + def ruby_to_tzinfo(ruby_tz_name) + TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[ruby_tz_name]) + end + + # Get the Ruby TimeZone equivalent to the TZInfo Timezone. + # @param [string] tzinfo_tz_name + # @return [string] Ruby Timezone + def tzinfo_to_ruby(tzinfo_tz_name) + ActiveSupport::TimeZone::MAPPING.invert[tzinfo_tz_name] + end + + end +end \ No newline at end of file diff --git a/vendor/assets/javascripts/moment-timezone-with-data.min.js b/vendor/assets/javascripts/moment-timezone-with-data.min.js new file mode 100755 index 00000000..fb128978 --- /dev/null +++ b/vendor/assets/javascripts/moment-timezone-with-data.min.js @@ -0,0 +1,7 @@ +//! moment-timezone.js +//! version : 0.3.0 +//! author : Tim Wood +//! license : MIT +//! github.com/moment/moment-timezone +!function(a,b){"use strict";"function"==typeof define&&define.amd?define(["moment"],b):"object"==typeof exports?module.exports=b(require("moment")):b(a.moment)}(this,function(a){"use strict";function b(a){return a>96?a-87:a>64?a-29:a-48}function c(a){var c,d=0,e=a.split("."),f=e[0],g=e[1]||"",h=1,i=0,j=1;for(45===a.charCodeAt(0)&&(d=1,j=-1),d;dc;c++)a[c]=Math.round((a[c-1]||0)+6e4*a[c]);a[b-1]=1/0}function f(a,b){var c,d=[];for(c=0;cB||2===B&&6>C)&&t("Moment Timezone requires Moment.js >= 2.6.0. You are using Moment.js "+a.version+". See momentjs.com"),h.prototype={_set:function(a){this.name=a.name,this.abbrs=a.abbrs,this.untils=a.untils,this.offsets=a.offsets},_index:function(a){var b,c=+a,d=this.untils;for(b=0;be;e++)if(b=g[e],c=g[e+1],d=g[e?e-1:e],c>b&&u.moveAmbiguousForward?b=c:b>d&&u.moveInvalidForward&&(b=d),fB||2===B&&9>C)&&t("Moment Timezone setDefault() requires Moment.js >= 2.9.0. You are using Moment.js "+a.version+"."),a.defaultZone=b?k(b):null,a};var E=a.momentProperties;return"[object Array]"===Object.prototype.toString.call(E)?(E.push("_z"),E.push("_a")):E&&(E._z=null),q({version:"2014j",zones:["Africa/Abidjan|LMT GMT|g.8 0|01|-2ldXH.Q","Africa/Accra|LMT GMT GHST|.Q 0 -k|012121212121212121212121212121212121212121212121|-26BbX.8 6tzX.8 MnE 1BAk MnE 1BAk MnE 1BAk MnE 1C0k MnE 1BAk MnE 1BAk MnE 1BAk MnE 1C0k MnE 1BAk MnE 1BAk MnE 1BAk MnE 1C0k MnE 1BAk MnE 1BAk MnE 1BAk MnE 1C0k MnE 1BAk MnE 1BAk MnE 1BAk MnE 1C0k MnE 1BAk MnE 1BAk MnE","Africa/Addis_Ababa|LMT EAT BEAT BEAUT|-2r.g -30 -2u -2J|01231|-1F3Cr.g 3Dzr.g okMu MFXJ","Africa/Algiers|PMT WET WEST CET CEST|-9.l 0 -10 -10 -20|0121212121212121343431312123431213|-2nco9.l cNb9.l HA0 19A0 1iM0 11c0 1oo0 Wo0 1rc0 QM0 1EM0 UM0 DA0 Imo0 rd0 De0 9Xz0 1fb0 1ap0 16K0 2yo0 mEp0 hwL0 jxA0 11A0 dDd0 17b0 11B0 1cN0 2Dy0 1cN0 1fB0 1cL0","Africa/Bangui|LMT WAT|-d.A -10|01|-22y0d.A","Africa/Bissau|LMT WAT GMT|12.k 10 0|012|-2ldWV.E 2xonV.E","Africa/Blantyre|LMT CAT|-2a.k -20|01|-2GJea.k","Africa/Cairo|EET EEST|-20 -30|010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-1bIO0 vb0 1ip0 11z0 1iN0 1nz0 12p0 1pz0 10N0 1pz0 16p0 1jz0 s3d0 Vz0 1oN0 11b0 1oO0 10N0 1pz0 10N0 1pb0 10N0 1pb0 10N0 1pb0 10N0 1pz0 10N0 1pb0 10N0 1pb0 11d0 1oL0 11d0 1pb0 11d0 1oL0 11d0 1oL0 11d0 1oL0 11d0 1pb0 11d0 1oL0 11d0 1oL0 11d0 1oL0 11d0 1pb0 11d0 1oL0 11d0 1oL0 11d0 1oL0 11d0 1pb0 11d0 1oL0 11d0 1WL0 rd0 1Rz0 wp0 1pb0 11d0 1oL0 11d0 1oL0 11d0 1oL0 11d0 1pb0 11d0 1qL0 Xd0 1oL0 11d0 1oL0 11d0 1pb0 11d0 1oL0 11d0 1oL0 11d0 1ny0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 WL0 1qN0 Rb0 1wp0 On0 1zd0 Lz0 1EN0 Fb0 c10 8n0 8Nd0 gL0 e10 mn0 1o10 jz0 gN0 pb0 1qN0 dX0 e10 xz0 1o10 bb0 e10 An0 1o10 5z0 e10 FX0 1o10 2L0 e10 IL0 1C10 Lz0 1wp0 TX0 1qN0 WL0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0","Africa/Casablanca|LMT WET WEST CET|u.k 0 -10 -10|012121212121212121312121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2gMnt.E 130Lt.E rb0 Dd0 dVb0 b6p0 TX0 EoB0 LL0 gnd0 rz0 43d0 AL0 1Nd0 XX0 1Cp0 pz0 dEp0 4mn0 SyN0 AL0 1Nd0 wn0 1FB0 Db0 1zd0 Lz0 1Nf0 wM0 co0 go0 1o00 s00 dA0 vc0 11A0 A00 e00 y00 11A0 uo0 e00 DA0 11A0 rA0 e00 Jc0 WM0 m00 gM0 M00 WM0 jc0 e00 RA0 11A0 dA0 e00 Uo0 11A0 800 gM0 Xc0 11A0 5c0 e00 17A0 WM0 2o0 e00 1ao0 19A0 1g00 16M0 1iM0 1400 1lA0 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qo0 1200 1kM0 14M0 1i00","Africa/Ceuta|WET WEST CET CEST|0 -10 -10 -20|010101010101010101010232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-25KN0 11z0 drd0 18o0 3I00 17c0 1fA0 1a00 1io0 1a00 1y7p0 LL0 gnd0 rz0 43d0 AL0 1Nd0 XX0 1Cp0 pz0 dEp0 4VB0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Africa/El_Aaiun|LMT WAT WET WEST|Q.M 10 0 -10|0123232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-1rDz7.c 1GVA7.c 6L0 AL0 1Nd0 XX0 1Cp0 pz0 1cBB0 AL0 1Nd0 wn0 1FB0 Db0 1zd0 Lz0 1Nf0 wM0 co0 go0 1o00 s00 dA0 vc0 11A0 A00 e00 y00 11A0 uo0 e00 DA0 11A0 rA0 e00 Jc0 WM0 m00 gM0 M00 WM0 jc0 e00 RA0 11A0 dA0 e00 Uo0 11A0 800 gM0 Xc0 11A0 5c0 e00 17A0 WM0 2o0 e00 1ao0 19A0 1g00 16M0 1iM0 1400 1lA0 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qo0 1200 1kM0 14M0 1i00","Africa/Johannesburg|SAST SAST SAST|-1u -20 -30|012121|-2GJdu 1Ajdu 1cL0 1cN0 1cL0","Africa/Juba|LMT CAT CAST EAT|-2a.8 -20 -30 -30|01212121212121212121212121212121213|-1yW2a.8 1zK0a.8 16L0 1iN0 17b0 1jd0 17b0 1ip0 17z0 1i10 17X0 1hB0 18n0 1hd0 19b0 1gp0 19z0 1iN0 17b0 1ip0 17z0 1i10 18n0 1hd0 18L0 1gN0 19b0 1gp0 19z0 1iN0 17z0 1i10 17X0 yGd0","Africa/Monrovia|MMT LRT GMT|H.8 I.u 0|012|-23Lzg.Q 29s01.m","Africa/Ndjamena|LMT WAT WAST|-10.c -10 -20|0121|-2le10.c 2J3c0.c Wn0","Africa/Tripoli|LMT CET CEST EET|-Q.I -10 -20 -20|012121213121212121212121213123123|-21JcQ.I 1hnBQ.I vx0 4iP0 xx0 4eN0 Bb0 7ip0 U0n0 A10 1db0 1cN0 1db0 1dd0 1db0 1eN0 1bb0 1e10 1cL0 1c10 1db0 1dd0 1db0 1cN0 1db0 1q10 fAn0 1ep0 1db0 AKq0 TA0 1o00","Africa/Tunis|PMT CET CEST|-9.l -10 -20|0121212121212121212121212121212121|-2nco9.l 18pa9.l 1qM0 DA0 3Tc0 11B0 1ze0 WM0 7z0 3d0 14L0 1cN0 1f90 1ar0 16J0 1gXB0 WM0 1rA0 11c0 nwo0 Ko0 1cM0 1cM0 1rA0 10M0 zuM0 10N0 1aN0 1qM0 WM0 1qM0 11A0 1o00","Africa/Windhoek|SWAT SAST SAST CAT WAT WAST|-1u -20 -30 -20 -10 -20|012134545454545454545454545454545454545454545454545454545454545454545454545454545454545454545|-2GJdu 1Ajdu 1cL0 1SqL0 9NA0 11D0 1nX0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 11B0 1nX0 11B0","America/Adak|NST NWT NPT BST BDT AHST HAST HADT|b0 a0 a0 b0 a0 a0 a0 90|012034343434343434343434343434343456767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676|-17SX0 8wW0 iB0 Qlb0 52O0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 cm0 10q0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Anchorage|CAT CAWT CAPT AHST AHDT YST AKST AKDT|a0 90 90 a0 90 90 90 80|012034343434343434343434343434343456767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676|-17T00 8wX0 iA0 Qlb0 52O0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 cm0 10q0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Anguilla|LMT AST|46.4 40|01|-2kNvR.U","America/Antigua|LMT EST AST|47.c 50 40|012|-2kNvQ.M 1yxAQ.M","America/Araguaina|LMT BRT BRST|3c.M 30 20|0121212121212121212121212121212121212121212121212121|-2glwL.c HdKL.c 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 dMN0 Lz0 1zd0 Rb0 1wN0 Wn0 1tB0 Rb0 1tB0 WL0 1tB0 Rb0 1zd0 On0 1HB0 FX0 ny10 Lz0","America/Argentina/Buenos_Aires|CMT ART ARST ART ARST|4g.M 40 30 30 20|0121212121212121212121212121212121212121213434343434343234343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wp0 Rb0 1wp0 TX0 g0p0 10M0 j3c0 uL0 1qN0 WL0","America/Argentina/Catamarca|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|0121212121212121212121212121212121212121213434343454343235343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wq0 Ra0 1wp0 TX0 g0p0 10M0 ako0 7B0 8zb0 uL0","America/Argentina/Cordoba|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|0121212121212121212121212121212121212121213434343454343234343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wq0 Ra0 1wp0 TX0 g0p0 10M0 j3c0 uL0 1qN0 WL0","America/Argentina/Jujuy|CMT ART ARST ART ARST WART WARST|4g.M 40 30 30 20 40 30|01212121212121212121212121212121212121212134343456543432343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1ze0 TX0 1ld0 WK0 1wp0 TX0 g0p0 10M0 j3c0 uL0","America/Argentina/La_Rioja|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|01212121212121212121212121212121212121212134343434534343235343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Qn0 qO0 16n0 Rb0 1wp0 TX0 g0p0 10M0 ako0 7B0 8zb0 uL0","America/Argentina/Mendoza|CMT ART ARST ART ARST WART WARST|4g.M 40 30 30 20 40 30|0121212121212121212121212121212121212121213434345656543235343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1u20 SL0 1vd0 Tb0 1wp0 TW0 g0p0 10M0 agM0 Op0 7TX0 uL0","America/Argentina/Rio_Gallegos|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|0121212121212121212121212121212121212121213434343434343235343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wp0 Rb0 1wp0 TX0 g0p0 10M0 ako0 7B0 8zb0 uL0","America/Argentina/Salta|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|01212121212121212121212121212121212121212134343434543432343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wq0 Ra0 1wp0 TX0 g0p0 10M0 j3c0 uL0","America/Argentina/San_Juan|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|01212121212121212121212121212121212121212134343434534343235343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Qn0 qO0 16n0 Rb0 1wp0 TX0 g0p0 10M0 ak00 m10 8lb0 uL0","America/Argentina/San_Luis|CMT ART ARST ART ARST WART WARST|4g.M 40 30 30 20 40 30|01212121212121212121212121212121212121212134343456536353465653|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 XX0 1q20 SL0 AN0 kin0 10M0 ak00 m10 8lb0 8L0 jd0 1qN0 WL0 1qN0","America/Argentina/Tucuman|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|012121212121212121212121212121212121212121343434345434323534343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wq0 Ra0 1wp0 TX0 g0p0 10M0 ako0 4N0 8BX0 uL0 1qN0 WL0","America/Argentina/Ushuaia|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|0121212121212121212121212121212121212121213434343434343235343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wp0 Rb0 1wp0 TX0 g0p0 10M0 ajA0 8p0 8zb0 uL0","America/Aruba|LMT ANT AST|4z.L 4u 40|012|-2kV7o.d 28KLS.d","America/Asuncion|AMT PYT PYT PYST|3O.E 40 30 30|012131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313|-1x589.k 1DKM9.k 3CL0 3Dd0 10L0 1pB0 10n0 1pB0 10n0 1pB0 1cL0 1dd0 1db0 1dd0 1cL0 1dd0 1cL0 1dd0 1cL0 1dd0 1db0 1dd0 1cL0 1dd0 1cL0 1dd0 1cL0 1dd0 1db0 1dd0 1cL0 1lB0 14n0 1dd0 1cL0 1fd0 WL0 1rd0 1aL0 1dB0 Xz0 1qp0 Xb0 1qN0 10L0 1rB0 TX0 1tB0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1qN0 1cL0 WN0 1qL0 11B0 1nX0 1ip0 WL0 1qN0 WL0 1qN0 WL0 1tB0 TX0 1tB0 TX0 1tB0 19X0 1a10 1fz0 1a10 1fz0 1cN0 17b0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1ip0 17b0 1ip0 17b0 1ip0","America/Atikokan|CST CDT CWT CPT EST|60 50 50 50 50|0101234|-25TQ0 1in0 Rnb0 3je0 8x30 iw0","America/Bahia|LMT BRT BRST|2y.4 30 20|01212121212121212121212121212121212121212121212121212121212121|-2glxp.U HdLp.U 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 1EN0 Lz0 1C10 IL0 1HB0 Db0 1HB0 On0 1zd0 On0 1zd0 Lz0 1zd0 Rb0 1wN0 Wn0 1tB0 Rb0 1tB0 WL0 1tB0 Rb0 1zd0 On0 1HB0 FX0 l5B0 Rb0","America/Bahia_Banderas|LMT MST CST PST MDT CDT|71 70 60 80 60 50|0121212131414141414141414141414141414152525252525252525252525252525252525252525252525252525252|-1UQF0 deL0 8lc0 17c0 10M0 1dd0 otX0 gmN0 P2N0 13Vd0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nW0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0","America/Barbados|LMT BMT AST ADT|3W.t 3W.t 40 30|01232323232|-1Q0I1.v jsM0 1ODC1.v IL0 1ip0 17b0 1ip0 17b0 1ld0 13b0","America/Belem|LMT BRT BRST|3d.U 30 20|012121212121212121212121212121|-2glwK.4 HdKK.4 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0","America/Belize|LMT CST CHDT CDT|5Q.M 60 5u 50|01212121212121212121212121212121212121212121212121213131|-2kBu7.c fPA7.c Onu 1zcu Rbu 1wou Rbu 1wou Rbu 1zcu Onu 1zcu Onu 1zcu Rbu 1wou Rbu 1wou Rbu 1wou Rbu 1zcu Onu 1zcu Onu 1zcu Rbu 1wou Rbu 1wou Rbu 1zcu Onu 1zcu Onu 1zcu Onu 1zcu Rbu 1wou Rbu 1wou Rbu 1zcu Onu 1zcu Onu 1zcu Rbu 1wou Rbu 1f0Mu qn0 lxB0 mn0","America/Blanc-Sablon|AST ADT AWT APT|40 30 30 30|010230|-25TS0 1in0 UGp0 8x50 iu0","America/Boa_Vista|LMT AMT AMST|42.E 40 30|0121212121212121212121212121212121|-2glvV.k HdKV.k 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 smp0 WL0 1tB0 2L0","America/Bogota|BMT COT COST|4U.g 50 40|0121|-2eb73.I 38yo3.I 2en0","America/Boise|PST PDT MST MWT MPT MDT|80 70 70 60 60 60|0101023425252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252|-261q0 1nX0 11B0 1nX0 8C10 JCL0 8x20 ix0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 Dd0 1Kn0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Cambridge_Bay|zzz MST MWT MPT MDDT MDT CST CDT EST|0 70 60 60 50 60 60 50 50|0123141515151515151515151515151515151515151515678651515151515151515151515151515151515151515151515151515151515151515151515151|-21Jc0 RO90 8x20 ix0 LCL0 1fA0 zgO0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11A0 1nX0 2K0 WQ0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Campo_Grande|LMT AMT AMST|3C.s 40 30|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212|-2glwl.w HdLl.w 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 1EN0 Lz0 1C10 IL0 1HB0 Db0 1HB0 On0 1zd0 On0 1zd0 Lz0 1zd0 Rb0 1wN0 Wn0 1tB0 Rb0 1tB0 WL0 1tB0 Rb0 1zd0 On0 1HB0 FX0 1C10 Lz0 1Ip0 HX0 1zd0 On0 1HB0 IL0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 Rb0 1zd0 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 Rb0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10 Lz0 1C10 On0 1zd0 Rb0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0","America/Cancun|LMT CST EST EDT CDT|5L.4 60 50 40 50|0123232341414141414141414141414141414141414141414141414141414141414141414141414141414141|-1UQG0 2q2o0 yLB0 1lb0 14p0 1lb0 14p0 Lz0 xB0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0","America/Caracas|CMT VET VET|4r.E 4u 40|0121|-2kV7w.k 28KM2.k 1IwOu","America/Cayenne|LMT GFT GFT|3t.k 40 30|012|-2mrwu.E 2gWou.E","America/Cayman|KMT EST|57.b 50|01|-2l1uQ.N","America/Chicago|CST CDT EST CWT CPT|60 50 50 50 50|01010101010101010101010101010101010102010101010103401010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261s0 1nX0 11B0 1nX0 1wp0 TX0 WN0 1qL0 1cN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 11B0 1Hz0 14p0 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 RB0 8x30 iw0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Chihuahua|LMT MST CST CDT MDT|74.k 70 60 50 60|0121212323241414141414141414141414141414141414141414141414141414141414141414141414141414141|-1UQF0 deL0 8lc0 17c0 10M0 1dd0 2zQN0 1lb0 14p0 1lb0 14q0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0","America/Costa_Rica|SJMT CST CDT|5A.d 60 50|0121212121|-1Xd6n.L 2lu0n.L Db0 1Kp0 Db0 pRB0 15b0 1kp0 mL0","America/Creston|MST PST|70 80|010|-29DR0 43B0","America/Cuiaba|LMT AMT AMST|3I.k 40 30|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212|-2glwf.E HdLf.E 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 1EN0 Lz0 1C10 IL0 1HB0 Db0 1HB0 On0 1zd0 On0 1zd0 Lz0 1zd0 Rb0 1wN0 Wn0 1tB0 Rb0 1tB0 WL0 1tB0 Rb0 1zd0 On0 1HB0 FX0 4a10 HX0 1zd0 On0 1HB0 IL0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 Rb0 1zd0 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 Rb0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10 Lz0 1C10 On0 1zd0 Rb0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0","America/Danmarkshavn|LMT WGT WGST GMT|1e.E 30 20 0|01212121212121212121212121212121213|-2a5WJ.k 2z5fJ.k 19U0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 DC0","America/Dawson|YST YDT YWT YPT YDDT PST PDT|90 80 80 80 70 80 70|0101023040565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565|-25TN0 1in0 1o10 13V0 Ser0 8x00 iz0 LCL0 1fA0 jrA0 fNd0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Dawson_Creek|PST PDT PWT PPT MST|80 70 70 70 70|0102301010101010101010101010101010101010101010101010101014|-25TO0 1in0 UGp0 8x10 iy0 3NB0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 ML0","America/Denver|MST MDT MWT MPT|70 60 60 60|01010101023010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261r0 1nX0 11B0 1nX0 11B0 1qL0 WN0 mn0 Ord0 8x20 ix0 LCN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Detroit|LMT CST EST EWT EPT EDT|5w.b 60 50 40 40 40|01234252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252|-2Cgir.N peqr.N 156L0 8x40 iv0 6fd0 11z0 Jy10 SL0 dnB0 1cL0 s10 1Vz0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Edmonton|LMT MST MDT MWT MPT|7x.Q 70 60 60 60|01212121212121341212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2yd4q.8 shdq.8 1in0 17d0 hz0 2dB0 1fz0 1a10 11z0 1qN0 WL0 1qN0 11z0 IGN0 8x20 ix0 3NB0 11z0 LFB0 1cL0 3Cp0 1cL0 66N0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Eirunepe|LMT ACT ACST AMT|4D.s 50 40 40|0121212121212121212121212121212131|-2glvk.w HdLk.w 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 dPB0 On0 yTd0 d5X0","America/El_Salvador|LMT CST CDT|5U.M 60 50|012121|-1XiG3.c 2Fvc3.c WL0 1qN0 WL0","America/Ensenada|LMT MST PST PDT PWT PPT|7M.4 70 80 70 70 70|012123245232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-1UQE0 4PX0 8mM0 8lc0 SN0 1cL0 pHB0 83r0 zI0 5O10 1Rz0 cOP0 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 BUp0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 U10 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Fort_Wayne|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|010101023010101010101010101040454545454545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 QI10 Db0 RB0 8x30 iw0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 5Tz0 1o10 qLb0 1cL0 1cN0 1cL0 1qhd0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Fortaleza|LMT BRT BRST|2y 30 20|0121212121212121212121212121212121212121|-2glxq HdLq 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 nsp0 WL0 1tB0 5z0 2mN0 On0","America/Glace_Bay|LMT AST ADT AWT APT|3X.M 40 30 30 30|012134121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2IsI0.c CwO0.c 1in0 UGp0 8x50 iu0 iq10 11z0 Jg10 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Godthab|LMT WGT WGST|3q.U 30 20|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2a5Ux.4 2z5dx.4 19U0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","America/Goose_Bay|NST NDT NST NDT NWT NPT AST ADT ADDT|3u.Q 2u.Q 3u 2u 2u 2u 40 30 20|010232323232323245232323232323232323232323232323232323232326767676767676767676767676767676767676767676768676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676|-25TSt.8 1in0 DXb0 2HbX.8 WL0 1qN0 WL0 1qN0 WL0 1tB0 TX0 1tB0 WL0 1qN0 WL0 1qN0 7UHu itu 1tB0 WL0 1qN0 WL0 1qN0 WL0 1qN0 WL0 1tB0 WL0 1ld0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 S10 g0u 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14n1 1lb0 14p0 1nW0 11C0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zcX Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Grand_Turk|KMT EST EDT AST|57.b 50 40 40|0121212121212121212121212121212121212121212121212121212121212121212121212123|-2l1uQ.N 2HHBQ.N 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Guatemala|LMT CST CDT|62.4 60 50|0121212121|-24KhV.U 2efXV.U An0 mtd0 Nz0 ifB0 17b0 zDB0 11z0","America/Guayaquil|QMT ECT|5e 50|01|-1yVSK","America/Guyana|LMT GBGT GYT GYT GYT|3Q.E 3J 3J 30 40|01234|-2dvU7.k 24JzQ.k mlc0 Bxbf","America/Halifax|LMT AST ADT AWT APT|4e.o 40 30 30 30|0121212121212121212121212121212121212121212121212134121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2IsHJ.A xzzJ.A 1db0 3I30 1in0 3HX0 IL0 1E10 ML0 1yN0 Pb0 1Bd0 Mn0 1Bd0 Rz0 1w10 Xb0 1w10 LX0 1w10 Xb0 1w10 Lz0 1C10 Jz0 1E10 OL0 1yN0 Un0 1qp0 Xb0 1qp0 11X0 1w10 Lz0 1HB0 LX0 1C10 FX0 1w10 Xb0 1qp0 Xb0 1BB0 LX0 1td0 Xb0 1qp0 Xb0 Rf0 8x50 iu0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 3Qp0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 3Qp0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 6i10 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Havana|HMT CST CDT|5t.A 50 40|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1Meuu.o 72zu.o ML0 sld0 An0 1Nd0 Db0 1Nd0 An0 6Ep0 An0 1Nd0 An0 JDd0 Mn0 1Ap0 On0 1fd0 11X0 1qN0 WL0 1wp0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 14n0 1ld0 14L0 1kN0 15b0 1kp0 1cL0 1cN0 1fz0 1a10 1fz0 1fB0 11z0 14p0 1nX0 11B0 1nX0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 14n0 1ld0 14n0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 1a10 1in0 1a10 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 17c0 1o00 11A0 1qM0 11A0 1o00 11A0 1o00 14o0 1lc0 14o0 1lc0 11A0 6i00 Rc0 1wo0 U00 1tA0 Rc0 1wo0 U00 1wo0 U00 1zc0 U00 1qM0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0","America/Hermosillo|LMT MST CST PST MDT|7n.Q 70 60 80 60|0121212131414141|-1UQF0 deL0 8lc0 17c0 10M0 1dd0 otX0 gmN0 P2N0 13Vd0 1lb0 14p0 1lb0 14p0 1lb0","America/Indiana/Knox|CST CDT CWT CPT EST|60 50 50 50 50|0101023010101010101010101010101010101040101010101010101010101010101010101010101010101010141010101010101010101010101010101010101010101010101010101010101010|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 3NB0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 1cL0 1cN0 11z0 1o10 11z0 1o10 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 3Cn0 8wp0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 z8o0 1o00 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Indiana/Marengo|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|0101023010101010101010104545454545414545454545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 dyN0 11z0 6fd0 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 jrz0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1VA0 LA0 1BX0 1e6p0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Indiana/Petersburg|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|01010230101010101010101010104010101010101010101010141014545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 njX0 WN0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 3Fb0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 19co0 1o00 Rd0 1zb0 Oo0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Indiana/Tell_City|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|01010230101010101010101010101010454541010101010101010101010101010101010101010101010101010101010101010|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 1o10 11z0 g0p0 11z0 1o10 11z0 1qL0 WN0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 WL0 1qN0 1cL0 1cN0 1cL0 1cN0 caL0 1cL0 1cN0 1cL0 1qhd0 1o00 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Indiana/Vevay|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|010102304545454545454545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 kPB0 Awn0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1lnd0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Indiana/Vincennes|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|01010230101010101010101010101010454541014545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 1o10 11z0 g0p0 11z0 1o10 11z0 1qL0 WN0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 WL0 1qN0 1cL0 1cN0 1cL0 1cN0 caL0 1cL0 1cN0 1cL0 1qhd0 1o00 Rd0 1zb0 Oo0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Indiana/Winamac|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|01010230101010101010101010101010101010454541054545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 1cL0 1cN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 jrz0 1cL0 1cN0 1cL0 1qhd0 1o00 Rd0 1za0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Inuvik|zzz PST PDDT MST MDT|0 80 60 70 60|0121343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343|-FnA0 tWU0 1fA0 wPe0 2pz0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Iqaluit|zzz EWT EPT EST EDDT EDT CST CDT|0 40 40 50 30 40 60 50|01234353535353535353535353535353535353535353567353535353535353535353535353535353535353535353535353535353535353535353535353|-16K00 7nX0 iv0 LCL0 1fA0 zgO0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11C0 1nX0 11A0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Jamaica|KMT EST EDT|57.b 50 40|0121212121212121212121|-2l1uQ.N 2uM1Q.N 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0","America/Juneau|PST PWT PPT PDT YDT YST AKST AKDT|80 70 70 70 80 90 90 80|01203030303030303030303030403030356767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676|-17T20 8x10 iy0 Vo10 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cM0 1cM0 1cL0 1cN0 1fz0 1a10 1fz0 co0 10q0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Kentucky/Louisville|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|0101010102301010101010101010101010101454545454545414545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 3Fd0 Nb0 LPd0 11z0 RB0 8x30 iw0 Bb0 10N0 2bB0 8in0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 xz0 gso0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1VA0 LA0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Kentucky/Monticello|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|0101023010101010101010101010101010101010101010101010101010101010101010101454545454545454545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 SWp0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11A0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/La_Paz|CMT BOST BOT|4w.A 3w.A 40|012|-1x37r.o 13b0","America/Lima|LMT PET PEST|58.A 50 40|0121212121212121|-2tyGP.o 1bDzP.o zX0 1aN0 1cL0 1cN0 1cL0 1PrB0 zX0 1O10 zX0 6Gp0 zX0 98p0 zX0","America/Los_Angeles|PST PDT PWT PPT|80 70 70 70|010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261q0 1nX0 11B0 1nX0 SgN0 8x10 iy0 5Wp0 1Vb0 3dB0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Maceio|LMT BRT BRST|2m.Q 30 20|012121212121212121212121212121212121212121|-2glxB.8 HdLB.8 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 dMN0 Lz0 8Q10 WL0 1tB0 5z0 2mN0 On0","America/Managua|MMT CST EST CDT|5J.c 60 50 50|0121313121213131|-1quie.M 1yAMe.M 4mn0 9Up0 Dz0 1K10 Dz0 s3F0 1KH0 DB0 9In0 k8p0 19X0 1o30 11y0","America/Manaus|LMT AMT AMST|40.4 40 30|01212121212121212121212121212121|-2glvX.U HdKX.U 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 dPB0 On0","America/Martinique|FFMT AST ADT|44.k 40 30|0121|-2mPTT.E 2LPbT.E 19X0","America/Matamoros|LMT CST CDT|6E 60 50|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1UQG0 2FjC0 1nX0 i6p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 U10 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Mazatlan|LMT MST CST PST MDT|75.E 70 60 80 60|0121212131414141414141414141414141414141414141414141414141414141414141414141414141414141414141|-1UQF0 deL0 8lc0 17c0 10M0 1dd0 otX0 gmN0 P2N0 13Vd0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0","America/Menominee|CST CDT CWT CPT EST|60 50 50 50 50|01010230101041010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 1o10 11z0 LCN0 1fz0 6410 9Jb0 1cM0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Merida|LMT CST EST CDT|5W.s 60 50 50|0121313131313131313131313131313131313131313131313131313131313131313131313131313131313131|-1UQG0 2q2o0 2hz0 wu30 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0","America/Metlakatla|PST PWT PPT PDT|80 70 70 70|0120303030303030303030303030303030|-17T20 8x10 iy0 Vo10 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0","America/Mexico_City|LMT MST CST CDT CWT|6A.A 70 60 50 50|012121232324232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-1UQF0 deL0 8lc0 17c0 10M0 1dd0 gEn0 TX0 3xd0 Jb0 6zB0 SL0 e5d0 17b0 1Pff0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0","America/Miquelon|LMT AST PMST PMDT|3I.E 40 30 20|012323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-2mKkf.k 2LTAf.k gQ10 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Moncton|EST AST ADT AWT APT|50 40 30 30 30|012121212121212121212134121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2IsH0 CwN0 1in0 zAo0 An0 1Nd0 An0 1Nd0 An0 1Nd0 An0 1Nd0 An0 1Nd0 An0 1K10 Lz0 1zB0 NX0 1u10 Wn0 S20 8x50 iu0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 3Cp0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14n1 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 ReX 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Monterrey|LMT CST CDT|6F.g 60 50|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1UQG0 2FjC0 1nX0 i6p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0","America/Montevideo|MMT UYT UYHST UYST UYT UYHST|3I.I 3u 30 20 30 2u|012121212121212121212121213434343434345454543453434343434343434343434343434343434343434343434343434343434343434343434343434343434343|-20UIf.g 8jzJ.g 1cLu 1dcu 1cLu 1dcu 1cLu ircu 11zu 1o0u 11zu 1o0u 11zu 1qMu WLu 1qMu WLu 1qMu WLu 1qMu 11zu 1o0u 11zu NAu 11bu 2iMu zWu Dq10 19X0 pd0 jz0 cm10 19X0 1fB0 1on0 11d0 1oL0 1nB0 1fzu 1aou 1fzu 1aou 1fzu 3nAu Jb0 3MN0 1SLu 4jzu 2PB0 Lb0 3Dd0 1pb0 ixd0 An0 1MN0 An0 1wp0 On0 1wp0 Rb0 1zd0 On0 1wp0 Rb0 s8p0 1fB0 1ip0 11z0 1ld0 14n0 1o10 11z0 1o10 11z0 1o10 14n0 1ld0 14n0 1ld0 14n0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 14n0 1ld0 14n0 1ld0 14n0 1o10 11z0 1o10 11z0 1o10 14n0 1ld0 14n0 1ld0 14n0 1ld0 14n0 1o10 11z0 1o10 11z0 1o10 14n0 1ld0 14n0 1ld0 14n0 1o10 11z0 1o10 11z0 1o10 14n0 1ld0 14n0 1ld0 14n0 1ld0 14n0 1o10 11z0 1o10 11z0 1o10","America/Montreal|EST EDT EWT EPT|50 40 40 40|01010101010101010101010101010101010101010101012301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-28tR0 bV0 2m30 1in0 121u 1nb0 1g10 11z0 1o0u 11zu 1o0u 11zu 3VAu Rzu 1qMu WLu 1qMu WLu 1qKu WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 4kO0 8x40 iv0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 1cL0 1cN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Nassau|LMT EST EDT|59.u 50 40|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2kNuO.u 26XdO.u 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/New_York|EST EDT EWT EPT|50 40 40 40|01010101010101010101010101010101010101010101010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261t0 1nX0 11B0 1nX0 11B0 1qL0 1a10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 RB0 8x40 iv0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Nipigon|EST EDT EWT EPT|50 40 40 40|010123010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-25TR0 1in0 Rnb0 3je0 8x40 iv0 19yN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Nome|NST NWT NPT BST BDT YST AKST AKDT|b0 a0 a0 b0 a0 90 90 80|012034343434343434343434343434343456767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676|-17SX0 8wW0 iB0 Qlb0 52O0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 cl0 10q0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Noronha|LMT FNT FNST|29.E 20 10|0121212121212121212121212121212121212121|-2glxO.k HdKO.k 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 nsp0 WL0 1tB0 2L0 2pB0 On0","America/North_Dakota/Beulah|MST MDT MWT MPT CST CDT|70 60 60 60 60 50|010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101014545454545454545454545454545454545454545454545454545454|-261r0 1nX0 11B0 1nX0 SgN0 8x20 ix0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Oo0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/North_Dakota/Center|MST MDT MWT MPT CST CDT|70 60 60 60 60 50|010102301010101010101010101010101010101010101010101010101014545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-261r0 1nX0 11B0 1nX0 SgN0 8x20 ix0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14o0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/North_Dakota/New_Salem|MST MDT MWT MPT CST CDT|70 60 60 60 60 50|010102301010101010101010101010101010101010101010101010101010101010101010101010101454545454545454545454545454545454545454545454545454545454545454545454|-261r0 1nX0 11B0 1nX0 SgN0 8x20 ix0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14o0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Ojinaga|LMT MST CST CDT MDT|6V.E 70 60 50 60|0121212323241414141414141414141414141414141414141414141414141414141414141414141414141414141|-1UQF0 deL0 8lc0 17c0 10M0 1dd0 2zQN0 1lb0 14p0 1lb0 14q0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 U10 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Panama|CMT EST|5j.A 50|01|-2uduE.o","America/Pangnirtung|zzz AST AWT APT ADDT ADT EDT EST CST CDT|0 40 30 30 20 30 40 50 60 50|012314151515151515151515151515151515167676767689767676767676767676767676767676767676767676767676767676767676767676767676767|-1XiM0 PnG0 8x50 iu0 LCL0 1fA0 zgO0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1o00 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11C0 1nX0 11A0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Paramaribo|LMT PMT PMT NEGT SRT SRT|3E.E 3E.Q 3E.A 3u 3u 30|012345|-2nDUj.k Wqo0.c qanX.I 1dmLN.o lzc0","America/Phoenix|MST MDT MWT|70 60 60|01010202010|-261r0 1nX0 11B0 1nX0 SgN0 4Al1 Ap0 1db0 SWqX 1cL0","America/Port-au-Prince|PPMT EST EDT|4N 50 40|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-28RHb 2FnMb 19X0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14q0 1o00 11A0 1o00 11A0 1o00 14o0 1lc0 14o0 1lc0 14o0 1o00 11A0 1o00 11A0 1o00 14o0 1lc0 14o0 1lc0 i6n0 1nX0 11B0 1nX0 d430 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Porto_Acre|LMT ACT ACST AMT|4v.c 50 40 40|01212121212121212121212121212131|-2glvs.M HdLs.M 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 NBd0 d5X0","America/Porto_Velho|LMT AMT AMST|4f.A 40 30|012121212121212121212121212121|-2glvI.o HdKI.o 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0","America/Puerto_Rico|AST AWT APT|40 30 30|0120|-17lU0 7XT0 iu0","America/Rainy_River|CST CDT CWT CPT|60 50 50 50|010123010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-25TQ0 1in0 Rnb0 3je0 8x30 iw0 19yN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Rankin_Inlet|zzz CST CDDT CDT EST|0 60 40 50 50|012131313131313131313131313131313131313131313431313131313131313131313131313131313131313131313131313131313131313131313131|-vDc0 keu0 1fA0 zgO0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Recife|LMT BRT BRST|2j.A 30 20|0121212121212121212121212121212121212121|-2glxE.o HdLE.o 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 nsp0 WL0 1tB0 2L0 2pB0 On0","America/Regina|LMT MST MDT MWT MPT CST|6W.A 70 60 60 60 60|012121212121212121212121341212121212121212121212121215|-2AD51.o uHe1.o 1in0 s2L0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 66N0 1cL0 1cN0 19X0 1fB0 1cL0 1fB0 1cL0 1cN0 1cL0 M30 8x20 ix0 1ip0 1cL0 1ip0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 3NB0 1cL0 1cN0","America/Resolute|zzz CST CDDT CDT EST|0 60 40 50 50|012131313131313131313131313131313131313131313431313131313431313131313131313131313131313131313131313131313131313131313131|-SnA0 GWS0 1fA0 zgO0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Santa_Isabel|LMT MST PST PDT PWT PPT|7D.s 70 80 70 70 70|012123245232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-1UQE0 4PX0 8mM0 8lc0 SN0 1cL0 pHB0 83r0 zI0 5O10 1Rz0 cOP0 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 BUp0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0","America/Santarem|LMT AMT AMST BRT|3C.M 40 30 30|0121212121212121212121212121213|-2glwl.c HdLl.c 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 NBd0","America/Santiago|SMT CLT CLT CLST CLST|4G.K 50 40 40 30|010203131313131313124242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424|-2q5Th.e fNch.e 5gLG.K 21bh.e jRAG.K 1pbh.e 11d0 1oL0 11d0 1oL0 11d0 1oL0 11d0 1pb0 11d0 nHX0 op0 9UK0 1Je0 Qen0 WL0 1zd0 On0 1ip0 11z0 1o10 11z0 1qN0 WL0 1ld0 14n0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 WL0 1qN0 1cL0 1cN0 11z0 1ld0 14n0 1qN0 11z0 1cN0 19X0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 17b0 1ip0 11z0 1ip0 1fz0 1fB0 11z0 1qN0 WL0 1qN0 WL0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 17b0 1ip0 11z0 1o10 19X0 1fB0 1nX0 G10 1EL0 Op0 1zb0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1zb0 Op0 1zb0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1zb0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1zb0 Op0 1zb0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1zb0 Op0 1zb0 Rd0 1wn0 Rd0","America/Santo_Domingo|SDMT EST EDT EHDT AST|4E 50 40 4u 40|01213131313131414|-1ttjk 1lJMk Mn0 6sp0 Lbu 1Cou yLu 1RAu wLu 1QMu xzu 1Q0u xXu 1PAu 13jB0 e00","America/Sao_Paulo|LMT BRT BRST|36.s 30 20|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212|-2glwR.w HdKR.w 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 pTd0 PX0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 1EN0 Lz0 1C10 IL0 1HB0 Db0 1HB0 On0 1zd0 On0 1zd0 Lz0 1zd0 Rb0 1wN0 Wn0 1tB0 Rb0 1tB0 WL0 1tB0 Rb0 1zd0 On0 1HB0 FX0 1C10 Lz0 1Ip0 HX0 1zd0 On0 1HB0 IL0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 Rb0 1zd0 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 Rb0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10 Lz0 1C10 On0 1zd0 Rb0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0","America/Scoresbysund|LMT CGT CGST EGST EGT|1r.Q 20 10 0 10|0121343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434|-2a5Ww.8 2z5ew.8 1a00 1cK0 1cL0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","America/Sitka|PST PWT PPT PDT YST AKST AKDT|80 70 70 70 90 90 80|01203030303030303030303030303030345656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565|-17T20 8x10 iy0 Vo10 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 co0 10q0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/St_Johns|NST NDT NST NDT NWT NPT NDDT|3u.Q 2u.Q 3u 2u 2u 2u 1u|01010101010101010101010101010101010102323232323232324523232323232323232323232323232323232323232323232323232323232323232323232323232323232326232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-28oit.8 14L0 1nB0 1in0 1gm0 Dz0 1JB0 1cL0 1cN0 1cL0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1fB0 1cL0 1cN0 1cL0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1fB0 1cL0 1fB0 19X0 1fB0 19X0 10O0 eKX.8 19X0 1iq0 WL0 1qN0 WL0 1qN0 WL0 1tB0 TX0 1tB0 WL0 1qN0 WL0 1qN0 7UHu itu 1tB0 WL0 1qN0 WL0 1qN0 WL0 1qN0 WL0 1tB0 WL0 1ld0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14n1 1lb0 14p0 1nW0 11C0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zcX Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Swift_Current|LMT MST MDT MWT MPT CST|7b.k 70 60 60 60 60|012134121212121212121215|-2AD4M.E uHdM.E 1in0 UGp0 8x20 ix0 1o10 17b0 1ip0 11z0 1o10 11z0 1o10 11z0 isN0 1cL0 3Cp0 1cL0 1cN0 11z0 1qN0 WL0 pMp0","America/Tegucigalpa|LMT CST CDT|5M.Q 60 50|01212121|-1WGGb.8 2ETcb.8 WL0 1qN0 WL0 GRd0 AL0","America/Thule|LMT AST ADT|4z.8 40 30|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2a5To.Q 31NBo.Q 1cL0 1cN0 1cL0 1fB0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Thunder_Bay|CST EST EWT EPT EDT|60 50 40 40 40|0123141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141|-2q5S0 1iaN0 8x40 iv0 XNB0 1cL0 1cN0 1fz0 1cN0 1cL0 3Cp0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Toronto|EST EDT EWT EPT|50 40 40 40|01010101010101010101010101010101010101010101012301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-25TR0 1in0 11Wu 1nzu 1fD0 WJ0 1wr0 Nb0 1Ap0 On0 1zd0 On0 1wp0 TX0 1tB0 TX0 1tB0 TX0 1tB0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 4kM0 8x40 iv0 1o10 11z0 1nX0 11z0 1o10 11z0 1o10 1qL0 11D0 1nX0 11B0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Vancouver|PST PDT PWT PPT|80 70 70 70|0102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-25TO0 1in0 UGp0 8x10 iy0 1o10 17b0 1ip0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Whitehorse|YST YDT YWT YPT YDDT PST PDT|90 80 80 80 70 80 70|0101023040565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565|-25TN0 1in0 1o10 13V0 Ser0 8x00 iz0 LCL0 1fA0 1Be0 xDz0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Winnipeg|CST CDT CWT CPT|60 50 50 50|010101023010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aIi0 WL0 3ND0 1in0 Jap0 Rb0 aCN0 8x30 iw0 1tB0 11z0 1ip0 11z0 1o10 11z0 1o10 11z0 1rd0 10L0 1op0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 1cL0 1cN0 11z0 6i10 WL0 6i10 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1a00 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1a00 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 14o0 1lc0 14o0 1o00 11A0 1o00 11A0 1o00 14o0 1lc0 14o0 1lc0 14o0 1o00 11A0 1o00 11A0 1o00 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1o00 11A0 1o00 11A0 1o00 14o0 1lc0 14o0 1lc0 14o0 1o00 11A0 1o00 11A0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Yakutat|YST YWT YPT YDT AKST AKDT|90 80 80 80 90 80|01203030303030303030303030303030304545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-17T10 8x00 iz0 Vo10 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 cn0 10q0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Yellowknife|zzz MST MWT MPT MDDT MDT|0 70 60 60 50 60|012314151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151|-1pdA0 hix0 8x20 ix0 LCL0 1fA0 zgO0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","Antarctica/Casey|zzz AWST CAST|0 -80 -b0|012121|-2q00 1DjS0 T90 40P0 KL0","Antarctica/Davis|zzz DAVT DAVT|0 -70 -50|01012121|-vyo0 iXt0 alj0 1D7v0 VB0 3Wn0 KN0","Antarctica/DumontDUrville|zzz PMT DDUT|0 -a0 -a0|0102|-U0o0 cfq0 bFm0","Antarctica/Macquarie|AEST AEDT zzz MIST|-a0 -b0 0 -b0|0102010101010101010101010101010101010101010101010101010101010101010101010101010101010101013|-29E80 19X0 4SL0 1ayy0 Lvs0 1cM0 1o00 Rc0 1wo0 Rc0 1wo0 U00 1wo0 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 11A0 1qM0 WM0 1qM0 Oo0 1zc0 Oo0 1zc0 Oo0 1wo0 WM0 1tA0 WM0 1tA0 U00 1tA0 U00 1tA0 11A0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 11A0 1o00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1cM0 1cM0 1cM0","Antarctica/Mawson|zzz MAWT MAWT|0 -60 -50|012|-CEo0 2fyk0","Antarctica/McMurdo|NZMT NZST NZST NZDT|-bu -cu -c0 -d0|01020202020202020202020202023232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323|-1GCVu Lz0 1tB0 11zu 1o0u 11zu 1o0u 11zu 1o0u 14nu 1lcu 14nu 1lcu 1lbu 11Au 1nXu 11Au 1nXu 11Au 1nXu 11Au 1nXu 11Au 1qLu WMu 1qLu 11Au 1n1bu IM0 1C00 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1qM0 14o0 1lc0 14o0 1lc0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1io0 17c0 1lc0 14o0 1lc0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1lc0 14o0 1lc0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1io0 17c0 1io0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00","Antarctica/Palmer|zzz ARST ART ART ARST CLT CLST|0 30 40 30 20 40 30|012121212123435656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656|-cao0 nD0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 jsN0 14N0 11z0 1o10 11z0 1qN0 WL0 1qN0 WL0 1qN0 1cL0 1cN0 11z0 1ld0 14n0 1qN0 11z0 1cN0 19X0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 17b0 1ip0 11z0 1ip0 1fz0 1fB0 11z0 1qN0 WL0 1qN0 WL0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 17b0 1ip0 11z0 1o10 19X0 1fB0 1nX0 G10 1EL0 Op0 1zb0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1zb0 Op0 1zb0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1zb0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1zb0 Op0 1zb0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1zb0 Op0 1zb0 Rd0 1wn0 Rd0","Antarctica/Rothera|zzz ROTT|0 30|01|gOo0","Antarctica/Syowa|zzz SYOT|0 -30|01|-vs00","Antarctica/Troll|zzz UTC CEST|0 0 -20|01212121212121212121212121212121212121212121212121212121212121212121|1puo0 hd0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Antarctica/Vostok|zzz VOST|0 -60|01|-tjA0","Arctic/Longyearbyen|CET CEST|-10 -20|010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2awM0 Qm0 W6o0 5pf0 WM0 1fA0 1cM0 1cM0 1cM0 1cM0 wJc0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1qM0 WM0 zpc0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Asia/Aden|LMT AST|-2X.S -30|01|-MG2X.S","Asia/Almaty|LMT ALMT ALMT ALMST|-57.M -50 -60 -70|0123232323232323232323232323232323232323232323232|-1Pc57.M eUo7.M 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 3Cl0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0","Asia/Amman|LMT EET EEST|-2n.I -20 -30|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1yW2n.I 1HiMn.I KL0 1oN0 11b0 1oN0 11b0 1pd0 1dz0 1cp0 11b0 1op0 11b0 fO10 1db0 1e10 1cL0 1cN0 1cL0 1cN0 1fz0 1pd0 10n0 1ld0 14n0 1hB0 15b0 1ip0 19X0 1cN0 1cL0 1cN0 17b0 1ld0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1So0 y00 1fc0 1dc0 1co0 1dc0 1cM0 1cM0 1cM0 1o00 11A0 1lc0 17c0 1cM0 1cM0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 4bX0 Dd0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0","Asia/Anadyr|LMT ANAT ANAT ANAST ANAST ANAST ANAT|-bN.U -c0 -d0 -e0 -d0 -c0 -b0|01232414141414141414141561414141414141414141414141414141414141561|-1PcbN.U eUnN.U 23CL0 1db0 1cN0 1dc0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qN0 WM0","Asia/Aqtau|LMT FORT FORT SHET SHET SHEST AQTT AQTST AQTST AQTT|-3l.4 -40 -50 -50 -60 -60 -50 -60 -50 -40|012345353535353535353536767676898989898989898989896|-1Pc3l.4 eUnl.4 1jcL0 JDc0 1cL0 1dc0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 2UK0 Fz0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cN0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 RW0","Asia/Aqtobe|LMT AKTT AKTT AKTST AKTT AQTT AQTST|-3M.E -40 -50 -60 -60 -50 -60|01234323232323232323232565656565656565656565656565|-1Pc3M.E eUnM.E 23CL0 1db0 1cM0 1dc0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 2UK0 Fz0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0","Asia/Ashgabat|LMT ASHT ASHT ASHST ASHST TMT TMT|-3R.w -40 -50 -60 -50 -40 -50|012323232323232323232324156|-1Pc3R.w eUnR.w 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 ba0 xC0","Asia/Baghdad|BMT AST ADT|-2V.A -30 -40|012121212121212121212121212121212121212121212121212121|-26BeV.A 2ACnV.A 11b0 1cp0 1dz0 1dd0 1db0 1cN0 1cp0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1de0 1dc0 1dc0 1dc0 1cM0 1dc0 1cM0 1dc0 1cM0 1dc0 1dc0 1dc0 1cM0 1dc0 1cM0 1dc0 1cM0 1dc0 1dc0 1dc0 1cM0 1dc0 1cM0 1dc0 1cM0 1dc0 1dc0 1dc0 1cM0 1dc0 1cM0 1dc0 1cM0 1dc0","Asia/Bahrain|LMT GST AST|-3m.k -40 -30|012|-21Jfm.k 27BXm.k","Asia/Baku|LMT BAKT BAKT BAKST BAKST AZST AZT AZT AZST|-3j.o -30 -40 -50 -40 -40 -30 -40 -50|0123232323232323232323245657878787878787878787878787878787878787878787878787878787878787878787878787878787878787|-1Pc3j.o 1jUoj.o WCL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 10K0 c30 1cJ0 1cL0 8wu0 1o00 11z0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Asia/Bangkok|BMT ICT|-6G.4 -70|01|-218SG.4","Asia/Beirut|EET EEST|-20 -30|010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-21aq0 1on0 1410 1db0 19B0 1in0 1ip0 WL0 1lQp0 11b0 1oN0 11b0 1oN0 11b0 1pd0 11b0 1oN0 11b0 q6N0 En0 1oN0 11b0 1oN0 11b0 1oN0 11b0 1pd0 11b0 1oN0 11b0 1op0 11b0 dA10 17b0 1iN0 17b0 1iN0 17b0 1iN0 17b0 1vB0 SL0 1mp0 13z0 1iN0 17b0 1iN0 17b0 1jd0 12n0 1a10 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0","Asia/Bishkek|LMT FRUT FRUT FRUST FRUST KGT KGST KGT|-4W.o -50 -60 -70 -60 -50 -60 -60|01232323232323232323232456565656565656565656565656567|-1Pc4W.o eUnW.o 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 11c0 1tX0 17b0 1ip0 17b0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1cPu 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 T8u","Asia/Brunei|LMT BNT BNT|-7D.E -7u -80|012|-1KITD.E gDc9.E","Asia/Calcutta|HMT BURT IST IST|-5R.k -6u -5u -6u|01232|-18LFR.k 1unn.k HB0 7zX0","Asia/Chita|LMT YAKT YAKT YAKST YAKST YAKT IRKT|-7x.Q -80 -90 -a0 -90 -a0 -80|012323232323232323232324123232323232323232323232323232323232323256|-21Q7x.Q pAnx.Q 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Asia/Choibalsan|LMT ULAT ULAT CHOST CHOT CHOT|-7C -70 -80 -a0 -90 -80|012343434343434343434343434343434343434343434345|-2APHC 2UkoC cKn0 1da0 1dd0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 6hD0 11z0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 3Db0","Asia/Chongqing|CST CDT|-80 -90|01010101010101010|-1c1I0 LX0 16p0 1jz0 1Myp0 Rb0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0","Asia/Colombo|MMT IST IHST IST LKT LKT|-5j.w -5u -60 -6u -6u -60|01231451|-2zOtj.w 1rFbN.w 1zzu 7Apu 23dz0 11zu n3cu","Asia/Dacca|HMT BURT IST DACT BDT BDST|-5R.k -6u -5u -60 -60 -70|01213454|-18LFR.k 1unn.k HB0 m6n0 LqMu 1x6n0 1i00","Asia/Damascus|LMT EET EEST|-2p.c -20 -30|01212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-21Jep.c Hep.c 17b0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1xRB0 11X0 1oN0 10L0 1pB0 11b0 1oN0 10L0 1mp0 13X0 1oN0 11b0 1pd0 11b0 1oN0 11b0 1oN0 11b0 1oN0 11b0 1pd0 11b0 1oN0 11b0 1oN0 11b0 1oN0 11b0 1pd0 11b0 1oN0 Nb0 1AN0 Nb0 bcp0 19X0 1gp0 19X0 3ld0 1xX0 Vd0 1Bz0 Sp0 1vX0 10p0 1dz0 1cN0 1cL0 1db0 1db0 1g10 1an0 1ap0 1db0 1fd0 1db0 1cN0 1db0 1dd0 1db0 1cp0 1dz0 1c10 1dX0 1cN0 1db0 1dd0 1db0 1cN0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1db0 1cN0 1db0 1cN0 19z0 1fB0 1qL0 11B0 1on0 Wp0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0","Asia/Dili|LMT TLT JST TLT WITA|-8m.k -80 -90 -90 -80|012343|-2le8m.k 1dnXm.k 8HA0 1ew00 Xld0","Asia/Dubai|LMT GST|-3F.c -40|01|-21JfF.c","Asia/Dushanbe|LMT DUST DUST DUSST DUSST TJT|-4z.c -50 -60 -70 -60 -50|0123232323232323232323245|-1Pc4z.c eUnz.c 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 14N0","Asia/Gaza|EET EET EEST IST IDT|-20 -30 -30 -20 -30|010101010102020202020202020202023434343434343434343434343430202020202020202020202020202020202020202020202020202020202020202020202020202020202020|-1c2q0 5Rb0 10r0 1px0 10N0 1pz0 16p0 1jB0 16p0 1jx0 pBd0 Vz0 1oN0 11b0 1oO0 10N0 1pz0 10N0 1pb0 10N0 1pb0 10N0 1pb0 10N0 1pz0 10N0 1pb0 10N0 1pb0 11d0 1oL0 dW0 hfB0 Db0 1fB0 Rb0 npB0 11z0 1C10 IL0 1s10 10n0 1o10 WL0 1zd0 On0 1ld0 11z0 1o10 14n0 1o10 14n0 1nd0 12n0 1nd0 Xz0 1q10 12n0 M10 C00 17c0 1io0 17c0 1io0 17c0 1o00 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 17c0 1io0 18N0 1bz0 19z0 1gp0 1610 1iL0 11z0 1o10 14o0 1lA1 SKX 1xd1 MKX 1AN0 1a00 1fA0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 19X0 1fB0 19X0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 19X0 1fB0 19X0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1fB0 1cL0 1cN0 1cL0","Asia/Hebron|EET EET EEST IST IDT|-20 -30 -30 -20 -30|01010101010202020202020202020202343434343434343434343434343020202020202020202020202020202020202020202020202020202020202020202020202020202020202020|-1c2q0 5Rb0 10r0 1px0 10N0 1pz0 16p0 1jB0 16p0 1jx0 pBd0 Vz0 1oN0 11b0 1oO0 10N0 1pz0 10N0 1pb0 10N0 1pb0 10N0 1pb0 10N0 1pz0 10N0 1pb0 10N0 1pb0 11d0 1oL0 dW0 hfB0 Db0 1fB0 Rb0 npB0 11z0 1C10 IL0 1s10 10n0 1o10 WL0 1zd0 On0 1ld0 11z0 1o10 14n0 1o10 14n0 1nd0 12n0 1nd0 Xz0 1q10 12n0 M10 C00 17c0 1io0 17c0 1io0 17c0 1o00 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 17c0 1io0 18N0 1bz0 19z0 1gp0 1610 1iL0 12L0 1mN0 14o0 1lc0 Tb0 1xd1 MKX bB0 cn0 1cN0 1a00 1fA0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 19X0 1fB0 19X0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 19X0 1fB0 19X0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1fB0 1cL0 1cN0 1cL0","Asia/Ho_Chi_Minh|LMT PLMT ICT IDT JST|-76.E -76.u -70 -80 -90|0123423232|-2yC76.E bK00.a 1h7b6.u 5lz0 18o0 3Oq0 k5b0 aW00 BAM0","Asia/Hong_Kong|LMT HKT HKST JST|-7A.G -80 -90 -90|0121312121212121212121212121212121212121212121212121212121212121212121|-2CFHA.G 1sEP6.G 1cL0 ylu 93X0 1qQu 1tX0 Rd0 1In0 NB0 1cL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1kL0 14N0 1nX0 U10 1tz0 U10 1wn0 Rd0 1wn0 U10 1tz0 U10 1tz0 U10 1tz0 U10 1wn0 Rd0 1wn0 Rd0 1wn0 U10 1tz0 U10 1tz0 17d0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 s10 1Vz0 1cN0 1cL0 1cN0 1cL0 6fd0 14n0","Asia/Hovd|LMT HOVT HOVT HOVST|-66.A -60 -70 -80|01232323232323232323232323232323232323232323232|-2APG6.A 2Uko6.A cKn0 1db0 1dd0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 6hD0 11z0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0","Asia/Irkutsk|IMT IRKT IRKT IRKST IRKST IRKT|-6V.5 -70 -80 -90 -80 -90|012323232323232323232324123232323232323232323232323232323232323252|-21zGV.5 pjXV.5 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Asia/Istanbul|IMT EET EEST TRST TRT|-1U.U -20 -30 -40 -30|012121212121212121212121212121212121212121212121212121234343434342121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2ogNU.U dzzU.U 11b0 8tB0 1on0 1410 1db0 19B0 1in0 3Rd0 Un0 1oN0 11b0 zSp0 CL0 mN0 1Vz0 1gN0 1pz0 5Rd0 1fz0 1yp0 ML0 1kp0 17b0 1ip0 17b0 1fB0 19X0 1jB0 18L0 1ip0 17z0 qdd0 xX0 3S10 Tz0 dA10 11z0 1o10 11z0 1qN0 11z0 1ze0 11B0 WM0 1qO0 WI0 1nX0 1rB0 10L0 11B0 1in0 17d0 1in0 2pX0 19E0 1fU0 16Q0 1iI0 16Q0 1iI0 1Vd0 pb0 3Kp0 14o0 1df0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cL0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WO0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 Xc0 1qo0 WM0 1qM0 11A0 1o00 1200 1nA0 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Asia/Jakarta|BMT JAVT WIB JST WIB WIB|-77.c -7k -7u -90 -80 -70|01232425|-1Q0Tk luM0 mPzO 8vWu 6kpu 4PXu xhcu","Asia/Jayapura|LMT WIT ACST|-9m.M -90 -9u|0121|-1uu9m.M sMMm.M L4nu","Asia/Jerusalem|JMT IST IDT IDDT|-2k.E -20 -30 -40|01212121212132121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-26Bek.E SyMk.E 5Rb0 10r0 1px0 10N0 1pz0 16p0 1jB0 16p0 1jx0 3LB0 Em0 or0 1cn0 1dB0 16n0 10O0 1ja0 1tC0 14o0 1cM0 1a00 11A0 1Na0 An0 1MP0 AJ0 1Kp0 LC0 1oo0 Wl0 EQN0 Db0 1fB0 Rb0 npB0 11z0 1C10 IL0 1s10 10n0 1o10 WL0 1zd0 On0 1ld0 11z0 1o10 14n0 1o10 14n0 1nd0 12n0 1nd0 Xz0 1q10 12n0 1hB0 1dX0 1ep0 1aL0 1eN0 17X0 1nf0 11z0 1tB0 19W0 1e10 17b0 1ep0 1gL0 18N0 1fz0 1eN0 17b0 1gq0 1gn0 19d0 1dz0 1c10 17X0 1hB0 1gn0 19d0 1dz0 1c10 17X0 1kp0 1dz0 1c10 1aL0 1eN0 1oL0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0 W10 1rz0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0 W10 1rz0 10N0 1oL0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0 W10 1rz0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0 W10 1rz0 W10 1rz0 10N0 1oL0 10N0 1oL0","Asia/Kabul|AFT AFT|-40 -4u|01|-10Qs0","Asia/Kamchatka|LMT PETT PETT PETST PETST|-ay.A -b0 -c0 -d0 -c0|01232323232323232323232412323232323232323232323232323232323232412|-1SLKy.A ivXy.A 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qN0 WM0","Asia/Karachi|LMT IST IST KART PKT PKST|-4s.c -5u -6u -50 -50 -60|012134545454|-2xoss.c 1qOKW.c 7zX0 eup0 LqMu 1fy01 1cL0 dK0X 11b0 1610 1jX0","Asia/Kashgar|LMT XJT|-5O.k -60|01|-1GgtO.k","Asia/Kathmandu|LMT IST NPT|-5F.g -5u -5J|012|-21JhF.g 2EGMb.g","Asia/Khandyga|LMT YAKT YAKT YAKST YAKST VLAT VLAST VLAT YAKT|-92.d -80 -90 -a0 -90 -a0 -b0 -b0 -a0|01232323232323232323232412323232323232323232323232565656565656565782|-21Q92.d pAp2.d 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 qK0 yN0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 17V0 7zD0","Asia/Krasnoyarsk|LMT KRAT KRAT KRAST KRAST KRAT|-6b.q -60 -70 -80 -70 -80|012323232323232323232324123232323232323232323232323232323232323252|-21Hib.q prAb.q 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Asia/Kuala_Lumpur|SMT MALT MALST MALT MALT JST MYT|-6T.p -70 -7k -7k -7u -90 -80|01234546|-2Bg6T.p 17anT.p 7hXE dM00 17bO 8Fyu 1so1u","Asia/Kuching|LMT BORT BORT BORTST JST MYT|-7l.k -7u -80 -8k -90 -80|01232323232323232425|-1KITl.k gDbP.k 6ynu AnE 1O0k AnE 1NAk AnE 1NAk AnE 1NAk AnE 1O0k AnE 1NAk AnE pAk 8Fz0 1so10","Asia/Kuwait|LMT AST|-3b.U -30|01|-MG3b.U","Asia/Macao|LMT MOT MOST CST|-7y.k -80 -90 -80|0121212121212121212121212121212121212121213|-2le7y.k 1XO34.k 1wn0 Rd0 1wn0 R9u 1wqu U10 1tz0 TVu 1tz0 17gu 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cJu 1cL0 1cN0 1fz0 1cN0 1cOu 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cJu 1cL0 1cN0 1fz0 1cN0 1cL0 KEp0","Asia/Magadan|LMT MAGT MAGT MAGST MAGST MAGT|-a3.c -a0 -b0 -c0 -b0 -c0|012323232323232323232324123232323232323232323232323232323232323251|-1Pca3.c eUo3.c 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Asia/Makassar|LMT MMT WITA JST|-7V.A -7V.A -80 -90|01232|-21JjV.A vfc0 myLV.A 8ML0","Asia/Manila|PHT PHST JST|-80 -90 -90|010201010|-1kJI0 AL0 cK10 65X0 mXB0 vX0 VK10 1db0","Asia/Muscat|LMT GST|-3S.o -40|01|-21JfS.o","Asia/Nicosia|LMT EET EEST|-2d.s -20 -30|01212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1Vc2d.s 2a3cd.s 1cL0 1qp0 Xz0 19B0 19X0 1fB0 1db0 1cp0 1cL0 1fB0 19X0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1o30 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Asia/Novokuznetsk|LMT KRAT KRAT KRAST KRAST NOVST NOVT NOVT|-5M.M -60 -70 -80 -70 -70 -60 -70|012323232323232323232324123232323232323232323232323232323232325672|-1PctM.M eULM.M 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qN0 WM0 8Hz0","Asia/Novosibirsk|LMT NOVT NOVT NOVST NOVST|-5v.E -60 -70 -80 -70|0123232323232323232323241232341414141414141414141414141414141414121|-21Qnv.E pAFv.E 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 ml0 Os0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Asia/Omsk|LMT OMST OMST OMSST OMSST OMST|-4R.u -50 -60 -70 -60 -70|012323232323232323232324123232323232323232323232323232323232323252|-224sR.u pMLR.u 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Asia/Oral|LMT URAT URAT URAST URAT URAST ORAT ORAST ORAT|-3p.o -40 -50 -60 -60 -50 -40 -50 -50|012343232323232323251516767676767676767676767676768|-1Pc3p.o eUnp.o 23CL0 1db0 1cM0 1dc0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1fA0 2UK0 Fz0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 RW0","Asia/Pontianak|LMT PMT WIB JST WIB WITA WIB|-7h.k -7h.k -7u -90 -80 -80 -70|012324256|-2ua7h.k XE00 munL.k 8Rau 6kpu 4PXu xhcu Wqnu","Asia/Pyongyang|LMT KST JCST JST KST|-8n -8u -90 -90 -90|01234|-2um8n 97XR 12FXu jdA0","Asia/Qatar|LMT GST AST|-3q.8 -40 -30|012|-21Jfq.8 27BXq.8","Asia/Qyzylorda|LMT KIZT KIZT KIZST KIZT QYZT QYZT QYZST|-4l.Q -40 -50 -60 -60 -50 -60 -70|012343232323232323232325676767676767676767676767676|-1Pc4l.Q eUol.Q 23CL0 1db0 1cM0 1dc0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 2UK0 dC0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0","Asia/Rangoon|RMT BURT JST MMT|-6o.E -6u -90 -6u|0123|-21Jio.E SmnS.E 7j9u","Asia/Riyadh|LMT AST|-36.Q -30|01|-TvD6.Q","Asia/Sakhalin|LMT JCST JST SAKT SAKST SAKST SAKT|-9u.M -90 -90 -b0 -c0 -b0 -a0|0123434343434343434343435634343434343565656565656565656565656565636|-2AGVu.M 1iaMu.M je00 1qFa0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o10 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Asia/Samarkand|LMT SAMT SAMT SAMST TAST UZST UZT|-4r.R -40 -50 -60 -60 -60 -50|01234323232323232323232356|-1Pc4r.R eUor.R 23CL0 1db0 1cM0 1dc0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 11x0 bf0","Asia/Seoul|LMT KST JCST JST KST KDT KDT|-8r.Q -8u -90 -90 -90 -9u -a0|01234151515151515146464|-2um8r.Q 97XV.Q 12FXu jjA0 kKo0 2I0u OL0 1FB0 Rb0 1qN0 TX0 1tB0 TX0 1tB0 TX0 1tB0 TX0 2ap0 12FBu 11A0 1o00 11A0","Asia/Singapore|SMT MALT MALST MALT MALT JST SGT SGT|-6T.p -70 -7k -7k -7u -90 -7u -80|012345467|-2Bg6T.p 17anT.p 7hXE dM00 17bO 8Fyu Mspu DTA0","Asia/Srednekolymsk|LMT MAGT MAGT MAGST MAGST MAGT SRET|-ae.Q -a0 -b0 -c0 -b0 -c0 -b0|012323232323232323232324123232323232323232323232323232323232323256|-1Pcae.Q eUoe.Q 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Asia/Taipei|JWST JST CST CDT|-80 -90 -80 -90|01232323232323232323232323232323232323232|-1iw80 joM0 1yo0 Tz0 1ip0 1jX0 1cN0 11b0 1oN0 11b0 1oN0 11b0 1oN0 11b0 10N0 1BX0 10p0 1pz0 10p0 1pz0 10p0 1db0 1dd0 1db0 1cN0 1db0 1cN0 1db0 1cN0 1db0 1BB0 ML0 1Bd0 ML0 uq10 1db0 1cN0 1db0 97B0 AL0","Asia/Tashkent|LMT TAST TAST TASST TASST UZST UZT|-4B.b -50 -60 -70 -60 -60 -50|01232323232323232323232456|-1Pc4B.b eUnB.b 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 11y0 bf0","Asia/Tbilisi|TBMT TBIT TBIT TBIST TBIST GEST GET GET GEST|-2X.b -30 -40 -50 -40 -40 -30 -40 -50|0123232323232323232323245656565787878787878787878567|-1Pc2X.b 1jUnX.b WCL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 3y0 19f0 1cK0 1cL0 1cN0 1cL0 1cN0 1cL0 1cM0 1cL0 1fB0 3Nz0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 An0 Os0 WM0","Asia/Tehran|LMT TMT IRST IRST IRDT IRDT|-3p.I -3p.I -3u -40 -50 -4u|01234325252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252|-2btDp.I 1d3c0 1huLT.I TXu 1pz0 sN0 vAu 1cL0 1dB0 1en0 pNB0 UL0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 64p0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0","Asia/Thimbu|LMT IST BTT|-5W.A -5u -60|012|-Su5W.A 1BGMs.A","Asia/Tokyo|JCST JST JDT|-90 -90 -a0|0121212121|-1iw90 pKq0 QL0 1lB0 13X0 1zB0 NX0 1zB0 NX0","Asia/Ulaanbaatar|LMT ULAT ULAT ULAST|-77.w -70 -80 -90|01232323232323232323232323232323232323232323232|-2APH7.w 2Uko7.w cKn0 1db0 1dd0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 6hD0 11z0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0","Asia/Ust-Nera|LMT YAKT YAKT MAGST MAGT MAGST MAGT MAGT VLAT VLAT|-9w.S -80 -90 -c0 -b0 -b0 -a0 -c0 -b0 -a0|0123434343434343434343456434343434343434343434343434343434343434789|-21Q9w.S pApw.S 23CL0 1d90 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 17V0 7zD0","Asia/Vladivostok|LMT VLAT VLAT VLAST VLAST VLAT|-8L.v -90 -a0 -b0 -a0 -b0|012323232323232323232324123232323232323232323232323232323232323252|-1SJIL.v itXL.v 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Asia/Yakutsk|LMT YAKT YAKT YAKST YAKST YAKT|-8C.W -80 -90 -a0 -90 -a0|012323232323232323232324123232323232323232323232323232323232323252|-21Q8C.W pAoC.W 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Asia/Yekaterinburg|LMT PMT SVET SVET SVEST SVEST YEKT YEKST YEKT|-42.x -3J.5 -40 -50 -60 -50 -50 -60 -60|0123434343434343434343435267676767676767676767676767676767676767686|-2ag42.x 7mQh.s qBvJ.5 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Asia/Yerevan|LMT YERT YERT YERST YERST AMST AMT AMT AMST|-2W -30 -40 -50 -40 -40 -30 -40 -50|0123232323232323232323245656565657878787878787878787878787878787|-1Pc2W 1jUnW WCL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1am0 2r0 1cJ0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 3Fb0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0","Atlantic/Azores|HMT AZOT AZOST AZOMT AZOT AZOST WET|1S.w 20 10 0 10 0 0|01212121212121212121212121212121212121212121232123212321232121212121212121212121212121212121212121454545454545454545454545454545456545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-2ldW5.s aPX5.s Sp0 LX0 1vc0 Tc0 1uM0 SM0 1vc0 Tc0 1vc0 SM0 1vc0 6600 1co0 3E00 17c0 1fA0 1a00 1io0 1a00 1io0 17c0 3I00 17c0 1cM0 1cM0 3Fc0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Dc0 1tA0 1cM0 1dc0 1400 gL0 IM0 s10 U00 dX0 Rc0 pd0 Rc0 gL0 Oo0 pd0 Rc0 gL0 Oo0 pd0 14o0 1cM0 1cP0 1cM0 1cM0 1cM0 1cM0 1cM0 3Co0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 qIl0 1cM0 1fA0 1cM0 1cM0 1cN0 1cL0 1cN0 1cM0 1cM0 1cM0 1cM0 1cN0 1cL0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cL0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Atlantic/Bermuda|LMT AST ADT|4j.i 40 30|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1BnRE.G 1LTbE.G 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","Atlantic/Canary|LMT CANT WET WEST|11.A 10 0 -10|01232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-1UtaW.o XPAW.o 1lAK0 1a10 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Atlantic/Cape_Verde|LMT CVT CVST CVT|1y.4 20 10 10|01213|-2xomp.U 1qOMp.U 7zX0 1djf0","Atlantic/Faeroe|LMT WET WEST|r.4 0 -10|01212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2uSnw.U 2Wgow.U 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Atlantic/Madeira|FMT MADT MADST MADMT WET WEST|17.A 10 0 -10 0 -10|01212121212121212121212121212121212121212121232123212321232121212121212121212121212121212121212121454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-2ldWQ.o aPWQ.o Sp0 LX0 1vc0 Tc0 1uM0 SM0 1vc0 Tc0 1vc0 SM0 1vc0 6600 1co0 3E00 17c0 1fA0 1a00 1io0 1a00 1io0 17c0 3I00 17c0 1cM0 1cM0 3Fc0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Dc0 1tA0 1cM0 1dc0 1400 gL0 IM0 s10 U00 dX0 Rc0 pd0 Rc0 gL0 Oo0 pd0 Rc0 gL0 Oo0 pd0 14o0 1cM0 1cP0 1cM0 1cM0 1cM0 1cM0 1cM0 3Co0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 qIl0 1cM0 1fA0 1cM0 1cM0 1cN0 1cL0 1cN0 1cM0 1cM0 1cM0 1cM0 1cN0 1cL0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Atlantic/Reykjavik|RMT IST ISST GMT|1r.M 10 0 0|01212121212121212121212121212121212121212121212121212121212121213|-2uWmw.c mfaw.c 1Bd0 ML0 1LB0 NLX0 1pe0 zd0 1EL0 LA0 1C00 Oo0 1wo0 Rc0 1wo0 Rc0 1wo0 Rc0 1zc0 Oo0 1zc0 14o0 1lc0 14o0 1lc0 14o0 1o00 11A0 1lc0 14o0 1o00 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1o00 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1o00 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1o00 14o0","Atlantic/South_Georgia|GST|20|0|","Atlantic/Stanley|SMT FKT FKST FKT FKST|3P.o 40 30 30 20|0121212121212134343212121212121212121212121212121212121212121212121212|-2kJw8.A 12bA8.A 19X0 1fB0 19X0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1fB0 Cn0 1Cc10 WL0 1qL0 U10 1tz0 U10 1qM0 WN0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1tz0 U10 1tz0 WN0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1tz0 WN0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1qN0 U10 1wn0 Rd0 1wn0 U10 1tz0 U10 1tz0 U10 1tz0 U10 1tz0 U10 1wn0 U10 1tz0 U10 1tz0 U10","Australia/ACT|AEST AEDT|-a0 -b0|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101|-293lX xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 14o0 1o00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 U00 1qM0 WM0 1tA0 WM0 1tA0 U00 1tA0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 11A0 1o00 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 WM0 1qM0 14o0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0","Australia/Adelaide|ACST ACDT|-9u -au|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101|-293lt xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 U00 1qM0 WM0 1tA0 WM0 1tA0 U00 1tA0 U00 1tA0 Oo0 1zc0 WM0 1qM0 Rc0 1zc0 U00 1tA0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 WM0 1qM0 14o0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0","Australia/Brisbane|AEST AEDT|-a0 -b0|01010101010101010|-293lX xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 H1A0 Oo0 1zc0 Oo0 1zc0 Oo0","Australia/Broken_Hill|ACST ACDT|-9u -au|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101|-293lt xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 14o0 1o00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 U00 1qM0 WM0 1tA0 WM0 1tA0 U00 1tA0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 WM0 1qM0 14o0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0","Australia/Currie|AEST AEDT|-a0 -b0|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101|-29E80 19X0 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 11A0 1qM0 WM0 1qM0 Oo0 1zc0 Oo0 1zc0 Oo0 1wo0 WM0 1tA0 WM0 1tA0 U00 1tA0 U00 1tA0 11A0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 11A0 1o00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0","Australia/Darwin|ACST ACDT|-9u -au|010101010|-293lt xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0","Australia/Eucla|ACWST ACWDT|-8J -9J|0101010101010101010|-293kI xcX 10jd0 yL0 1cN0 1cL0 1gSp0 Oo0 l5A0 Oo0 iJA0 G00 zU00 IM0 1qM0 11A0 1o00 11A0","Australia/Hobart|AEST AEDT|-a0 -b0|010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101|-29E80 19X0 10jd0 yL0 1cN0 1cL0 1fB0 19X0 VfB0 1cM0 1o00 Rc0 1wo0 Rc0 1wo0 U00 1wo0 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 11A0 1qM0 WM0 1qM0 Oo0 1zc0 Oo0 1zc0 Oo0 1wo0 WM0 1tA0 WM0 1tA0 U00 1tA0 U00 1tA0 11A0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 11A0 1o00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0","Australia/LHI|AEST LHST LHDT LHDT|-a0 -au -bu -b0|0121212121313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313|raC0 1zdu Rb0 1zd0 On0 1zd0 On0 1zd0 On0 1zd0 TXu 1qMu WLu 1tAu WLu 1tAu TXu 1tAu Onu 1zcu Onu 1zcu Onu 1zcu Rbu 1zcu Onu 1zcu Onu 1zcu 11zu 1o0u 11zu 1o0u 11zu 1o0u 11zu 1qMu WLu 11Au 1nXu 1qMu 11zu 1o0u 11zu 1o0u 11zu 1qMu WLu 1qMu 11zu 1o0u WLu 1qMu 14nu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1fAu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1fAu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1fzu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1fAu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1fAu 1cLu 1cMu 1cLu 1cMu","Australia/Lindeman|AEST AEDT|-a0 -b0|010101010101010101010|-293lX xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 H1A0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0","Australia/Melbourne|AEST AEDT|-a0 -b0|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101|-293lX xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 U00 1qM0 WM0 1qM0 11A0 1tA0 U00 1tA0 U00 1tA0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 11A0 1o00 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 WM0 1qM0 14o0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0","Australia/Perth|AWST AWDT|-80 -90|0101010101010101010|-293jX xcX 10jd0 yL0 1cN0 1cL0 1gSp0 Oo0 l5A0 Oo0 iJA0 G00 zU00 IM0 1qM0 11A0 1o00 11A0","CET|CET CEST|-10 -20|01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aFe0 11d0 1iO0 11A0 1o00 11A0 Qrc0 6i00 WM0 1fA0 1cM0 1cM0 1cM0 16M0 1gMM0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","CST6CDT|CST CDT CWT CPT|60 50 50 50|010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","Chile/EasterIsland|EMT EASST EAST EAST EASST|7h.s 60 70 60 50|012121212121212121212121212121213434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434|-1uSgG.w nHUG.w op0 9UK0 RXB0 WL0 1zd0 On0 1ip0 11z0 1o10 11z0 1qN0 WL0 1ld0 14n0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 WL0 1qN0 1cL0 1cN0 11z0 1ld0 14n0 1qN0 11z0 1cN0 19X0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 17b0 1ip0 11z0 1ip0 1fz0 1fB0 11z0 1qN0 WL0 1qN0 WL0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 17b0 1ip0 11z0 1o10 19X0 1fB0 1nX0 G10 1EL0 Op0 1zb0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1zb0 Op0 1zb0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1zb0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1zb0 Op0 1zb0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1wn0 Rd0 1zb0 Op0 1zb0 Rd0 1wn0 Rd0","EET|EET EEST|-20 -30|010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|hDB0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","EST|EST|50|0|","EST5EDT|EST EDT EWT EPT|50 40 40 40|010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261t0 1nX0 11B0 1nX0 SgN0 8x40 iv0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","Eire|DMT IST GMT BST IST|p.l -y.D 0 -10 -10|01232323232324242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242|-2ax9y.D Rc0 1fzy.D 14M0 1fc0 1g00 1co0 1dc0 1co0 1oo0 1400 1dc0 19A0 1io0 1io0 WM0 1o00 14o0 1o00 17c0 1io0 17c0 1fA0 1a00 1lc0 17c0 1io0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1cM0 1io0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1a00 1io0 1qM0 Dc0 g5X0 14p0 1wn0 17d0 1io0 11A0 1o00 17c0 1fA0 1a00 1fA0 1cM0 1fA0 1a00 17c0 1fA0 1a00 1io0 17c0 1lc0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1a00 1a00 1qM0 WM0 1qM0 11A0 1o00 WM0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1tA0 IM0 90o0 U00 1tA0 U00 1tA0 U00 1tA0 U00 1tA0 WM0 1qM0 WM0 1qM0 WM0 1tA0 U00 1tA0 U00 1tA0 11z0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 14o0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Etc/GMT+0|GMT|0|0|","Etc/GMT+1|GMT+1|10|0|","Etc/GMT+10|GMT+10|a0|0|","Etc/GMT+11|GMT+11|b0|0|","Etc/GMT+12|GMT+12|c0|0|","Etc/GMT+2|GMT+2|20|0|","Etc/GMT+3|GMT+3|30|0|","Etc/GMT+4|GMT+4|40|0|","Etc/GMT+5|GMT+5|50|0|","Etc/GMT+6|GMT+6|60|0|","Etc/GMT+7|GMT+7|70|0|","Etc/GMT+8|GMT+8|80|0|","Etc/GMT+9|GMT+9|90|0|","Etc/GMT-1|GMT-1|-10|0|","Etc/GMT-10|GMT-10|-a0|0|","Etc/GMT-11|GMT-11|-b0|0|","Etc/GMT-12|GMT-12|-c0|0|","Etc/GMT-13|GMT-13|-d0|0|","Etc/GMT-14|GMT-14|-e0|0|","Etc/GMT-2|GMT-2|-20|0|","Etc/GMT-3|GMT-3|-30|0|","Etc/GMT-4|GMT-4|-40|0|","Etc/GMT-5|GMT-5|-50|0|","Etc/GMT-6|GMT-6|-60|0|","Etc/GMT-7|GMT-7|-70|0|","Etc/GMT-8|GMT-8|-80|0|","Etc/GMT-9|GMT-9|-90|0|","Etc/UCT|UCT|0|0|","Etc/UTC|UTC|0|0|","Europe/Amsterdam|AMT NST NEST NET CEST CET|-j.w -1j.w -1k -k -20 -10|010101010101010101010101010101010101010101012323234545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545|-2aFcj.w 11b0 1iP0 11A0 1io0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1co0 1io0 1yo0 Pc0 1a00 1fA0 1Bc0 Mo0 1tc0 Uo0 1tA0 U00 1uo0 W00 1s00 VA0 1so0 Vc0 1sM0 UM0 1wo0 Rc0 1u00 Wo0 1rA0 W00 1s00 VA0 1sM0 UM0 1w00 fV0 BCX.w 1tA0 U00 1u00 Wo0 1sm0 601k WM0 1fA0 1cM0 1cM0 1cM0 16M0 1gMM0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Andorra|WET CET CEST|0 -10 -20|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-UBA0 1xIN0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Athens|AMT EET EEST CEST CET|-1y.Q -20 -30 -20 -10|012123434121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2a61x.Q CNbx.Q mn0 kU10 9b0 3Es0 Xa0 1fb0 1dd0 k3X0 Nz0 SCp0 1vc0 SO0 1cM0 1a00 1ao0 1fc0 1a10 1fG0 1cg0 1dX0 1bX0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Belfast|GMT BST BDST|0 -10 -20|0101010101010101010101010101010101010101010101010121212121210101210101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2axa0 Rc0 1fA0 14M0 1fc0 1g00 1co0 1dc0 1co0 1oo0 1400 1dc0 19A0 1io0 1io0 WM0 1o00 14o0 1o00 17c0 1io0 17c0 1fA0 1a00 1lc0 17c0 1io0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1cM0 1io0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1a00 1io0 1qM0 Dc0 2Rz0 Dc0 1zc0 Oo0 1zc0 Rc0 1wo0 17c0 1iM0 FA0 xB0 1fA0 1a00 14o0 bb0 LA0 xB0 Rc0 1wo0 11A0 1o00 17c0 1fA0 1a00 1fA0 1cM0 1fA0 1a00 17c0 1fA0 1a00 1io0 17c0 1lc0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1a00 1a00 1qM0 WM0 1qM0 11A0 1o00 WM0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1tA0 IM0 90o0 U00 1tA0 U00 1tA0 U00 1tA0 U00 1tA0 WM0 1qM0 WM0 1qM0 WM0 1tA0 U00 1tA0 U00 1tA0 11z0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 14o0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Belgrade|CET CEST|-10 -20|01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-19RC0 3IP0 WM0 1fA0 1cM0 1cM0 1rc0 Qo0 1vmo0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Berlin|CET CEST CEMT|-10 -20 -30|01010101010101210101210101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aFe0 11d0 1iO0 11A0 1o00 11A0 Qrc0 6i00 WM0 1fA0 1cM0 1cM0 1cM0 kL0 Nc0 m10 WM0 1ao0 1cp0 dX0 jz0 Dd0 1io0 17c0 1fA0 1a00 1ehA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Bratislava|CET CEST|-10 -20|010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aFe0 11d0 1iO0 11A0 1o00 11A0 Qrc0 6i00 WM0 1fA0 1cM0 16M0 1lc0 1tA0 17A0 11c0 1io0 17c0 1io0 17c0 1fc0 1ao0 1bNc0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Brussels|WET CET CEST WEST|0 -10 -20 -10|0121212103030303030303030303030303030303030303030303212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2ehc0 3zX0 11c0 1iO0 11A0 1o00 11A0 my0 Ic0 1qM0 Rc0 1EM0 UM0 1u00 10o0 1io0 1io0 17c0 1a00 1fA0 1cM0 1cM0 1io0 17c0 1fA0 1a00 1io0 1a30 1io0 17c0 1fA0 1a00 1io0 17c0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Dc0 y00 5Wn0 WM0 1fA0 1cM0 16M0 1iM0 16M0 1C00 Uo0 1eeo0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Bucharest|BMT EET EEST|-1I.o -20 -30|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1xApI.o 20LI.o RA0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1Axc0 On0 1fA0 1a10 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cK0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cL0 1cN0 1cL0 1fB0 1nX0 11E0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Budapest|CET CEST|-10 -20|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aFe0 11d0 1iO0 11A0 1ip0 17b0 1op0 1tb0 Q2m0 3Ne0 WM0 1fA0 1cM0 1cM0 1oJ0 1dc0 1030 1fA0 1cM0 1cM0 1cM0 1cM0 1fA0 1a00 1iM0 1fA0 8Ha0 Rb0 1wN0 Rb0 1BB0 Lz0 1C20 LB0 SNX0 1a10 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Busingen|CET CEST|-10 -20|01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-19Lc0 11A0 1o00 11A0 1xG10 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Chisinau|CMT BMT EET EEST CEST CET MSK MSD|-1T -1I.o -20 -30 -20 -10 -30 -40|0123232323232323232345454676767676767676767623232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-26jdT wGMa.A 20LI.o RA0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 27A0 2en0 39g0 WM0 1fA0 1cM0 V90 1t7z0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1ty0 2bD0 1cM0 1cK0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1nX0 11E0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Copenhagen|CET CEST|-10 -20|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2azC0 Tz0 VuO0 60q0 WM0 1fA0 1cM0 1cM0 1cM0 S00 1HA0 Nc0 1C00 Dc0 1Nc0 Ao0 1h5A0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Gibraltar|GMT BST BDST CET CEST|0 -10 -20 -10 -20|010101010101010101010101010101010101010101010101012121212121010121010101010101010101034343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343|-2axa0 Rc0 1fA0 14M0 1fc0 1g00 1co0 1dc0 1co0 1oo0 1400 1dc0 19A0 1io0 1io0 WM0 1o00 14o0 1o00 17c0 1io0 17c0 1fA0 1a00 1lc0 17c0 1io0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1cM0 1io0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1a00 1io0 1qM0 Dc0 2Rz0 Dc0 1zc0 Oo0 1zc0 Rc0 1wo0 17c0 1iM0 FA0 xB0 1fA0 1a00 14o0 bb0 LA0 xB0 Rc0 1wo0 11A0 1o00 17c0 1fA0 1a00 1fA0 1cM0 1fA0 1a00 17c0 1fA0 1a00 1io0 17c0 1lc0 17c0 1fA0 10Jz0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Helsinki|HMT EET EEST|-1D.N -20 -30|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1WuND.N OULD.N 1dA0 1xGq0 1cM0 1cM0 1cM0 1cN0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Kaliningrad|CET CEST CET CEST MSK MSD EEST EET FET|-10 -20 -20 -30 -30 -40 -30 -20 -30|0101010101010232454545454545454545454676767676767676767676767676767676767676787|-2aFe0 11d0 1iO0 11A0 1o00 11A0 Qrc0 6i00 WM0 1fA0 1cM0 1cM0 Am0 Lb0 1en0 op0 1pNz0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 1cJ0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Europe/Kiev|KMT EET MSK CEST CET MSD EEST|-22.4 -20 -30 -20 -10 -40 -30|0123434252525252525252525256161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161|-1Pc22.4 eUo2.4 rnz0 2Hg0 WM0 1fA0 da0 1v4m0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 Db0 3220 1cK0 1cL0 1cN0 1cL0 1cN0 1cL0 1cQ0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Lisbon|LMT WET WEST WEMT CET CEST|A.J 0 -10 -20 -10 -20|012121212121212121212121212121212121212121212321232123212321212121212121212121212121212121212121214121212121212121212121212121212124545454212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2ldXn.f aPWn.f Sp0 LX0 1vc0 Tc0 1uM0 SM0 1vc0 Tc0 1vc0 SM0 1vc0 6600 1co0 3E00 17c0 1fA0 1a00 1io0 1a00 1io0 17c0 3I00 17c0 1cM0 1cM0 3Fc0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Dc0 1tA0 1cM0 1dc0 1400 gL0 IM0 s10 U00 dX0 Rc0 pd0 Rc0 gL0 Oo0 pd0 Rc0 gL0 Oo0 pd0 14o0 1cM0 1cP0 1cM0 1cM0 1cM0 1cM0 1cM0 3Co0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 pvy0 1cM0 1cM0 1fA0 1cM0 1cM0 1cN0 1cL0 1cN0 1cM0 1cM0 1cM0 1cM0 1cN0 1cL0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Luxembourg|LMT CET CEST WET WEST WEST WET|-o.A -10 -20 0 -10 -20 -10|0121212134343434343434343434343434343434343434343434565651212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2DG0o.A t6mo.A TB0 1nX0 Up0 1o20 11A0 rW0 CM0 1qP0 R90 1EO0 UK0 1u20 10m0 1ip0 1in0 17e0 19W0 1fB0 1db0 1cp0 1in0 17d0 1fz0 1a10 1in0 1a10 1in0 17f0 1fA0 1a00 1io0 17c0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Dc0 vA0 60L0 WM0 1fA0 1cM0 17c0 1io0 16M0 1C00 Uo0 1eeo0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Madrid|WET WEST WEMT CET CEST|0 -10 -20 -10 -20|01010101010101010101010121212121234343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343|-28dd0 11A0 1go0 19A0 1co0 1dA0 b1A0 18o0 3I00 17c0 1fA0 1a00 1io0 1a00 1io0 17c0 iyo0 Rc0 18o0 1hc0 1io0 1a00 14o0 5aL0 MM0 1vc0 17A0 1i00 1bc0 1eo0 17d0 1in0 17A0 6hA0 10N0 XIL0 1a10 1in0 17d0 19X0 1cN0 1fz0 1a10 1fX0 1cp0 1cO0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Malta|CET CEST|-10 -20|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2as10 M00 1cM0 1cM0 14o0 1o00 WM0 1qM0 17c0 1cM0 M3A0 5M20 WM0 1fA0 1cM0 1cM0 1cM0 16m0 1de0 1lc0 14m0 1lc0 WO0 1qM0 GTW0 On0 1C10 Lz0 1C10 Lz0 1EN0 Lz0 1C10 Lz0 1zd0 Oo0 1C00 On0 1cp0 1cM0 1lA0 Xc0 1qq0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1iN0 19z0 1fB0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Minsk|MMT EET MSK CEST CET MSD EEST FET|-1O -20 -30 -20 -10 -40 -30 -30|012343432525252525252525252616161616161616161616161616161616161616172|-1Pc1O eUnO qNX0 3gQ0 WM0 1fA0 1cM0 Al0 1tsn0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 3Fc0 1cN0 1cK0 1cM0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hy0","Europe/Monaco|PMT WET WEST WEMT CET CEST|-9.l 0 -10 -20 -10 -20|01212121212121212121212121212121212121212121212121232323232345454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-2nco9.l cNb9.l HA0 19A0 1iM0 11c0 1oo0 Wo0 1rc0 QM0 1EM0 UM0 1u00 10o0 1io0 1wo0 Rc0 1a00 1fA0 1cM0 1cM0 1io0 17c0 1fA0 1a00 1io0 1a00 1io0 17c0 1fA0 1a00 1io0 17c0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Df0 2RV0 11z0 11B0 1ze0 WM0 1fA0 1cM0 1fa0 1aq0 16M0 1ekn0 1cL0 1fC0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Moscow|MMT MMT MST MDST MSD MSK MSM EET EEST MSK|-2u.h -2v.j -3v.j -4v.j -40 -30 -50 -20 -30 -40|012132345464575454545454545454545458754545454545454545454545454545454545454595|-2ag2u.h 2pyW.W 1bA0 11X0 GN0 1Hb0 c20 imv.j 3DA0 dz0 15A0 c10 2q10 iM10 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rU0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Europe/Paris|PMT WET WEST CEST CET WEMT|-9.l 0 -10 -20 -10 -20|0121212121212121212121212121212121212121212121212123434352543434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434|-2nco8.l cNb8.l HA0 19A0 1iM0 11c0 1oo0 Wo0 1rc0 QM0 1EM0 UM0 1u00 10o0 1io0 1wo0 Rc0 1a00 1fA0 1cM0 1cM0 1io0 17c0 1fA0 1a00 1io0 1a00 1io0 17c0 1fA0 1a00 1io0 17c0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Df0 Ik0 5M30 WM0 1fA0 1cM0 Vx0 hB0 1aq0 16M0 1ekn0 1cL0 1fC0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Riga|RMT LST EET MSK CEST CET MSD EEST|-1A.y -2A.y -20 -30 -20 -10 -40 -30|010102345454536363636363636363727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272|-25TzA.y 11A0 1iM0 ko0 gWm0 yDXA.y 2bX0 3fE0 WM0 1fA0 1cM0 1cM0 4m0 1sLy0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 1o00 11A0 1o00 11A0 1qM0 3oo0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Rome|CET CEST|-10 -20|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2as10 M00 1cM0 1cM0 14o0 1o00 WM0 1qM0 17c0 1cM0 M3A0 5M20 WM0 1fA0 1cM0 16K0 1iO0 16m0 1de0 1lc0 14m0 1lc0 WO0 1qM0 GTW0 On0 1C10 Lz0 1C10 Lz0 1EN0 Lz0 1C10 Lz0 1zd0 Oo0 1C00 On0 1C10 Lz0 1zd0 On0 1C10 LA0 1C00 LA0 1zc0 Oo0 1C00 Oo0 1zc0 Oo0 1fC0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Samara|LMT SAMT SAMT KUYT KUYST MSD MSK EEST KUYT SAMST SAMST|-3k.k -30 -40 -40 -50 -40 -30 -30 -30 -50 -40|012343434343434343435656782929292929292929292929292929292929292a12|-22WNk.k qHak.k bcn0 1Qqo0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1fA0 1cM0 1cN0 8o0 14j0 1cL0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qN0 WM0","Europe/Simferopol|SMT EET MSK CEST CET MSD EEST MSK|-2g -20 -30 -20 -10 -40 -30 -40|012343432525252525252525252161616525252616161616161616161616161616161616172|-1Pc2g eUog rEn0 2qs0 WM0 1fA0 1cM0 3V0 1u0L0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1Q00 4eL0 1cL0 1cN0 1cL0 1cN0 dX0 WL0 1cN0 1cL0 1fB0 1o30 11B0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11z0 1nW0","Europe/Sofia|EET CET CEST EEST|-20 -10 -20 -30|01212103030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030|-168L0 WM0 1fA0 1cM0 1cM0 1cN0 1mKH0 1dd0 1fb0 1ap0 1fb0 1a20 1fy0 1a30 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cK0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1nX0 11E0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Stockholm|CET CEST|-10 -20|01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2azC0 TB0 2yDe0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Tallinn|TMT CET CEST EET MSK MSD EEST|-1D -10 -20 -20 -30 -40 -30|012103421212454545454545454546363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363|-26oND teD 11A0 1Ta0 4rXl KSLD 2FX0 2Jg0 WM0 1fA0 1cM0 18J0 1sTX0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o10 11A0 1qM0 5QM0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Tirane|LMT CET CEST|-1j.k -10 -20|01212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2glBj.k 14pcj.k 5LC0 WM0 4M0 1fCK0 10n0 1op0 11z0 1pd0 11z0 1qN0 WL0 1qp0 Xb0 1qp0 Xb0 1qp0 11z0 1lB0 11z0 1qN0 11z0 1iN0 16n0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Uzhgorod|CET CEST MSK MSD EET EEST|-10 -20 -30 -40 -20 -30|010101023232323232323232320454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-1cqL0 6i00 WM0 1fA0 1cM0 1ml0 1Cp0 1r3W0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1Q00 1Nf0 2pw0 1cL0 1cN0 1cL0 1cN0 1cL0 1cQ0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Vienna|CET CEST|-10 -20|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aFe0 11d0 1iO0 11A0 1o00 11A0 3KM0 14o0 LA00 6i00 WM0 1fA0 1cM0 1cM0 1cM0 400 2qM0 1a00 1cM0 1cM0 1io0 17c0 1gHa0 19X0 1cP0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Vilnius|WMT KMT CET EET MSK CEST MSD EEST|-1o -1z.A -10 -20 -30 -20 -40 -30|012324525254646464646464646464647373737373737352537373737373737373737373737373737373737373737373737373737373737373737373|-293do 6ILM.o 1Ooz.A zz0 Mfd0 29W0 3is0 WM0 1fA0 1cM0 LV0 1tgL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11B0 1o00 11A0 1qM0 8io0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Volgograd|LMT TSAT STAT STAT VOLT VOLST VOLST VOLT MSK MSK|-2V.E -30 -30 -40 -40 -50 -40 -30 -40 -30|012345454545454545454676748989898989898989898989898989898989898989|-21IqV.E cLXV.E cEM0 1gqn0 Lco0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1fA0 1cM0 2pz0 1cJ0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0","Europe/Warsaw|WMT CET CEST EET EEST|-1o -10 -20 -20 -30|012121234312121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2ctdo 1LXo 11d0 1iO0 11A0 1o00 11A0 1on0 11A0 6zy0 HWP0 5IM0 WM0 1fA0 1cM0 1dz0 1mL0 1en0 15B0 1aq0 1nA0 11A0 1io0 17c0 1fA0 1a00 iDX0 LA0 1cM0 1cM0 1C00 Oo0 1cM0 1cM0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1C00 LA0 uso0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","Europe/Zaporozhye|CUT EET MSK CEST CET MSD EEST|-2k -20 -30 -20 -10 -40 -30|01234342525252525252525252526161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161|-1Pc2k eUok rdb0 2RE0 WM0 1fA0 8m0 1v9a0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cK0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cQ0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","HST|HST|a0|0|","Indian/Chagos|LMT IOT IOT|-4N.E -50 -60|012|-2xosN.E 3AGLN.E","Indian/Christmas|CXT|-70|0|","Indian/Cocos|CCT|-6u|0|","Indian/Kerguelen|zzz TFT|0 -50|01|-MG00","Indian/Mahe|LMT SCT|-3F.M -40|01|-2yO3F.M","Indian/Maldives|MMT MVT|-4S -50|01|-olgS","Indian/Mauritius|LMT MUT MUST|-3O -40 -50|012121|-2xorO 34unO 14L0 12kr0 11z0","Indian/Reunion|LMT RET|-3F.Q -40|01|-2mDDF.Q","Kwajalein|MHT KWAT MHT|-b0 c0 -c0|012|-AX0 W9X0","MET|MET MEST|-10 -20|01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aFe0 11d0 1iO0 11A0 1o00 11A0 Qrc0 6i00 WM0 1fA0 1cM0 1cM0 1cM0 16M0 1gMM0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","MST|MST|70|0|","MST7MDT|MST MDT MWT MPT|70 60 60 60|010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261r0 1nX0 11B0 1nX0 SgN0 8x20 ix0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","NZ-CHAT|CHAST CHAST CHADT|-cf -cJ -dJ|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212|-WqAf 1adef IM0 1C00 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1qM0 14o0 1lc0 14o0 1lc0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1io0 17c0 1lc0 14o0 1lc0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1lc0 14o0 1lc0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1io0 17c0 1io0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00","PST8PDT|PST PDT PWT PPT|80 70 70 70|010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261q0 1nX0 11B0 1nX0 SgN0 8x10 iy0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","Pacific/Apia|LMT WSST SST SDT WSDT WSST|bq.U bu b0 a0 -e0 -d0|01232345454545454545454545454545454545454545454545454545454|-2nDMx.4 1yW03.4 2rRbu 1ff0 1a00 CI0 AQ0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00","Pacific/Bougainville|PGT JST BST|-a0 -90 -b0|0102|-16Wy0 7CN0 2MQp0","Pacific/Chuuk|CHUT|-a0|0|","Pacific/Efate|LMT VUT VUST|-bd.g -b0 -c0|0121212121212121212121|-2l9nd.g 2Szcd.g 1cL0 1oN0 10L0 1fB0 19X0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 Lz0 1Nd0 An0","Pacific/Enderbury|PHOT PHOT PHOT|c0 b0 -d0|012|nIc0 B8n0","Pacific/Fakaofo|TKT TKT|b0 -d0|01|1Gfn0","Pacific/Fiji|LMT FJT FJST|-bT.I -c0 -d0|012121212121212121212121212121212121212121212121212121212121212|-2bUzT.I 3m8NT.I LA0 1EM0 IM0 nJc0 LA0 1o00 Rc0 1wo0 Ao0 1Nc0 Ao0 1Q00 xz0 1SN0 uM0 1SM0 xA0 1SM0 uM0 1SM0 uM0 1SM0 uM0 1SM0 uM0 1SM0 xA0 1SM0 uM0 1SM0 uM0 1SM0 uM0 1SM0 uM0 1SM0 uM0 1SM0 xA0 1SM0 uM0 1SM0 uM0 1SM0 uM0 1SM0 uM0 1SM0 uM0 1VA0 uM0 1SM0 uM0 1SM0 uM0 1SM0 uM0 1SM0 uM0 1SM0","Pacific/Funafuti|TVT|-c0|0|","Pacific/Galapagos|LMT ECT GALT|5W.o 50 60|012|-1yVS1.A 2dTz1.A","Pacific/Gambier|LMT GAMT|8X.M 90|01|-2jof0.c","Pacific/Guadalcanal|LMT SBT|-aD.M -b0|01|-2joyD.M","Pacific/Guam|GST ChST|-a0 -a0|01|1fpq0","Pacific/Honolulu|HST HDT HST|au 9u a0|010102|-1thLu 8x0 lef0 8Pz0 46p0","Pacific/Kiritimati|LINT LINT LINT|aE a0 -e0|012|nIaE B8nk","Pacific/Kosrae|KOST KOST|-b0 -c0|010|-AX0 1bdz0","Pacific/Majuro|MHT MHT|-b0 -c0|01|-AX0","Pacific/Marquesas|LMT MART|9i 9u|01|-2joeG","Pacific/Midway|NST NDT BST SST|b0 a0 b0 b0|01023|-x3N0 An0 pJd0 EyM0","Pacific/Nauru|LMT NRT JST NRT|-b7.E -bu -90 -c0|01213|-1Xdn7.E PvzB.E 5RCu 1ouJu","Pacific/Niue|NUT NUT NUT|bk bu b0|012|-KfME 17y0a","Pacific/Norfolk|NMT NFT|-bc -bu|01|-Kgbc","Pacific/Noumea|LMT NCT NCST|-b5.M -b0 -c0|01212121|-2l9n5.M 2EqM5.M xX0 1PB0 yn0 HeP0 Ao0","Pacific/Pago_Pago|LMT NST BST SST|bm.M b0 b0 b0|0123|-2nDMB.c 2gVzB.c EyM0","Pacific/Palau|PWT|-90|0|","Pacific/Pitcairn|PNT PST|8u 80|01|18Vku","Pacific/Pohnpei|PONT|-b0|0|","Pacific/Port_Moresby|PGT|-a0|0|","Pacific/Rarotonga|CKT CKHST CKT|au 9u a0|012121212121212121212121212|lyWu IL0 1zcu Onu 1zcu Onu 1zcu Rbu 1zcu Onu 1zcu Onu 1zcu Onu 1zcu Onu 1zcu Onu 1zcu Rbu 1zcu Onu 1zcu Onu 1zcu Onu","Pacific/Saipan|MPT MPT ChST|-90 -a0 -a0|012|-AV0 1g2n0","Pacific/Tahiti|LMT TAHT|9W.g a0|01|-2joe1.I","Pacific/Tarawa|GILT|-c0|0|","Pacific/Tongatapu|TOT TOT TOST|-ck -d0 -e0|01212121|-1aB0k 2n5dk 15A0 1wo0 xz0 1Q10 xz0","Pacific/Wake|WAKT|-c0|0|","Pacific/Wallis|WFT|-c0|0|","WET|WET WEST|0 -10|010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|hDB0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00"],links:["Africa/Abidjan|Africa/Bamako","Africa/Abidjan|Africa/Banjul","Africa/Abidjan|Africa/Conakry","Africa/Abidjan|Africa/Dakar","Africa/Abidjan|Africa/Freetown","Africa/Abidjan|Africa/Lome","Africa/Abidjan|Africa/Nouakchott","Africa/Abidjan|Africa/Ouagadougou","Africa/Abidjan|Africa/Sao_Tome","Africa/Abidjan|Africa/Timbuktu","Africa/Abidjan|Atlantic/St_Helena","Africa/Addis_Ababa|Africa/Asmara","Africa/Addis_Ababa|Africa/Asmera","Africa/Addis_Ababa|Africa/Dar_es_Salaam","Africa/Addis_Ababa|Africa/Djibouti","Africa/Addis_Ababa|Africa/Kampala","Africa/Addis_Ababa|Africa/Mogadishu","Africa/Addis_Ababa|Africa/Nairobi","Africa/Addis_Ababa|Indian/Antananarivo","Africa/Addis_Ababa|Indian/Comoro","Africa/Addis_Ababa|Indian/Mayotte","Africa/Bangui|Africa/Brazzaville","Africa/Bangui|Africa/Douala","Africa/Bangui|Africa/Kinshasa","Africa/Bangui|Africa/Lagos","Africa/Bangui|Africa/Libreville","Africa/Bangui|Africa/Luanda","Africa/Bangui|Africa/Malabo","Africa/Bangui|Africa/Niamey","Africa/Bangui|Africa/Porto-Novo","Africa/Blantyre|Africa/Bujumbura","Africa/Blantyre|Africa/Gaborone","Africa/Blantyre|Africa/Harare","Africa/Blantyre|Africa/Kigali","Africa/Blantyre|Africa/Lubumbashi","Africa/Blantyre|Africa/Lusaka","Africa/Blantyre|Africa/Maputo","Africa/Cairo|Egypt","Africa/Johannesburg|Africa/Maseru","Africa/Johannesburg|Africa/Mbabane","Africa/Juba|Africa/Khartoum","Africa/Tripoli|Libya","America/Adak|America/Atka","America/Adak|US/Aleutian","America/Anchorage|US/Alaska","America/Anguilla|America/Dominica","America/Anguilla|America/Grenada","America/Anguilla|America/Guadeloupe","America/Anguilla|America/Marigot","America/Anguilla|America/Montserrat","America/Anguilla|America/Port_of_Spain","America/Anguilla|America/St_Barthelemy","America/Anguilla|America/St_Kitts","America/Anguilla|America/St_Lucia","America/Anguilla|America/St_Thomas","America/Anguilla|America/St_Vincent","America/Anguilla|America/Tortola","America/Anguilla|America/Virgin","America/Argentina/Buenos_Aires|America/Buenos_Aires","America/Argentina/Catamarca|America/Argentina/ComodRivadavia","America/Argentina/Catamarca|America/Catamarca","America/Argentina/Cordoba|America/Cordoba","America/Argentina/Cordoba|America/Rosario","America/Argentina/Jujuy|America/Jujuy","America/Argentina/Mendoza|America/Mendoza","America/Aruba|America/Curacao","America/Aruba|America/Kralendijk","America/Aruba|America/Lower_Princes","America/Atikokan|America/Coral_Harbour","America/Chicago|US/Central","America/Denver|America/Shiprock","America/Denver|Navajo","America/Denver|US/Mountain","America/Detroit|US/Michigan","America/Edmonton|Canada/Mountain","America/Ensenada|America/Tijuana","America/Ensenada|Mexico/BajaNorte","America/Fort_Wayne|America/Indiana/Indianapolis","America/Fort_Wayne|America/Indianapolis","America/Fort_Wayne|US/East-Indiana","America/Halifax|Canada/Atlantic","America/Havana|Cuba","America/Indiana/Knox|America/Knox_IN","America/Indiana/Knox|US/Indiana-Starke","America/Jamaica|Jamaica","America/Kentucky/Louisville|America/Louisville","America/Los_Angeles|US/Pacific","America/Los_Angeles|US/Pacific-New","America/Manaus|Brazil/West","America/Mazatlan|Mexico/BajaSur","America/Mexico_City|Mexico/General","America/New_York|US/Eastern","America/Noronha|Brazil/DeNoronha","America/Phoenix|US/Arizona","America/Porto_Acre|America/Rio_Branco","America/Porto_Acre|Brazil/Acre","America/Regina|Canada/East-Saskatchewan","America/Regina|Canada/Saskatchewan","America/Santiago|Chile/Continental","America/Sao_Paulo|Brazil/East","America/St_Johns|Canada/Newfoundland","America/Toronto|Canada/Eastern","America/Vancouver|Canada/Pacific","America/Whitehorse|Canada/Yukon","America/Winnipeg|Canada/Central","Antarctica/McMurdo|Antarctica/South_Pole","Antarctica/McMurdo|NZ","Antarctica/McMurdo|Pacific/Auckland","Arctic/Longyearbyen|Atlantic/Jan_Mayen","Arctic/Longyearbyen|Europe/Oslo","Asia/Ashgabat|Asia/Ashkhabad","Asia/Bangkok|Asia/Phnom_Penh","Asia/Bangkok|Asia/Vientiane","Asia/Calcutta|Asia/Kolkata","Asia/Chongqing|Asia/Chungking","Asia/Chongqing|Asia/Harbin","Asia/Chongqing|Asia/Shanghai","Asia/Chongqing|PRC","Asia/Dacca|Asia/Dhaka","Asia/Ho_Chi_Minh|Asia/Saigon","Asia/Hong_Kong|Hongkong","Asia/Istanbul|Europe/Istanbul","Asia/Istanbul|Turkey","Asia/Jerusalem|Asia/Tel_Aviv","Asia/Jerusalem|Israel","Asia/Kashgar|Asia/Urumqi","Asia/Kathmandu|Asia/Katmandu","Asia/Macao|Asia/Macau","Asia/Makassar|Asia/Ujung_Pandang","Asia/Nicosia|Europe/Nicosia","Asia/Seoul|ROK","Asia/Singapore|Singapore","Asia/Taipei|ROC","Asia/Tehran|Iran","Asia/Thimbu|Asia/Thimphu","Asia/Tokyo|Japan","Asia/Ulaanbaatar|Asia/Ulan_Bator","Atlantic/Faeroe|Atlantic/Faroe","Atlantic/Reykjavik|Iceland","Australia/ACT|Australia/Canberra","Australia/ACT|Australia/NSW","Australia/ACT|Australia/Sydney","Australia/Adelaide|Australia/South","Australia/Brisbane|Australia/Queensland","Australia/Broken_Hill|Australia/Yancowinna","Australia/Darwin|Australia/North","Australia/Hobart|Australia/Tasmania","Australia/LHI|Australia/Lord_Howe","Australia/Melbourne|Australia/Victoria","Australia/Perth|Australia/West","Chile/EasterIsland|Pacific/Easter","Eire|Europe/Dublin","Etc/GMT+0|Etc/GMT","Etc/GMT+0|Etc/GMT-0","Etc/GMT+0|Etc/GMT0","Etc/GMT+0|Etc/Greenwich","Etc/GMT+0|GMT","Etc/GMT+0|GMT+0","Etc/GMT+0|GMT-0","Etc/GMT+0|GMT0","Etc/GMT+0|Greenwich","Etc/UCT|UCT","Etc/UTC|Etc/Universal","Etc/UTC|Etc/Zulu","Etc/UTC|UTC","Etc/UTC|Universal","Etc/UTC|Zulu","Europe/Belfast|Europe/Guernsey","Europe/Belfast|Europe/Isle_of_Man","Europe/Belfast|Europe/Jersey","Europe/Belfast|Europe/London","Europe/Belfast|GB","Europe/Belfast|GB-Eire","Europe/Belgrade|Europe/Ljubljana","Europe/Belgrade|Europe/Podgorica","Europe/Belgrade|Europe/Sarajevo","Europe/Belgrade|Europe/Skopje","Europe/Belgrade|Europe/Zagreb","Europe/Bratislava|Europe/Prague","Europe/Busingen|Europe/Vaduz","Europe/Busingen|Europe/Zurich","Europe/Chisinau|Europe/Tiraspol","Europe/Helsinki|Europe/Mariehamn","Europe/Lisbon|Portugal","Europe/Moscow|W-SU","Europe/Rome|Europe/San_Marino","Europe/Rome|Europe/Vatican","Europe/Warsaw|Poland","Kwajalein|Pacific/Kwajalein","NZ-CHAT|Pacific/Chatham","Pacific/Chuuk|Pacific/Truk","Pacific/Chuuk|Pacific/Yap","Pacific/Honolulu|Pacific/Johnston","Pacific/Honolulu|US/Hawaii","Pacific/Pago_Pago|Pacific/Samoa","Pacific/Pago_Pago|US/Samoa","Pacific/Pohnpei|Pacific/Ponape"]}),a +}); \ No newline at end of file diff --git a/vendor/assets/javascripts/moment-with-locales.min.js b/vendor/assets/javascripts/moment-with-locales.min.js new file mode 100755 index 00000000..f6043488 --- /dev/null +++ b/vendor/assets/javascripts/moment-with-locales.min.js @@ -0,0 +1,10 @@ +//! moment.js +//! version : 2.9.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +(function(a){function b(a,b,c){switch(arguments.length){case 2:return null!=a?a:b;case 3:return null!=a?a:null!=b?b:c;default:throw new Error("Implement me")}}function c(a,b){return Bb.call(a,b)}function d(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function e(a){vb.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+a)}function f(a,b){var c=!0;return o(function(){return c&&(e(a),c=!1),b.apply(this,arguments)},b)}function g(a,b){sc[a]||(e(b),sc[a]=!0)}function h(a,b){return function(c){return r(a.call(this,c),b)}}function i(a,b){return function(c){return this.localeData().ordinal(a.call(this,c),b)}}function j(a,b){var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),f=a.clone().add(e,"months");return 0>b-f?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)}function k(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&12>b&&(b+=12),d||12!==b||(b=0),b):b}function l(){}function m(a,b){b!==!1&&H(a),p(this,a),this._d=new Date(+a._d),uc===!1&&(uc=!0,vb.updateOffset(this),uc=!1)}function n(a){var b=A(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;this._milliseconds=+k+1e3*j+6e4*i+36e5*h,this._days=+g+7*f,this._months=+e+3*d+12*c,this._data={},this._locale=vb.localeData(),this._bubble()}function o(a,b){for(var d in b)c(b,d)&&(a[d]=b[d]);return c(b,"toString")&&(a.toString=b.toString),c(b,"valueOf")&&(a.valueOf=b.valueOf),a}function p(a,b){var c,d,e;if("undefined"!=typeof b._isAMomentObject&&(a._isAMomentObject=b._isAMomentObject),"undefined"!=typeof b._i&&(a._i=b._i),"undefined"!=typeof b._f&&(a._f=b._f),"undefined"!=typeof b._l&&(a._l=b._l),"undefined"!=typeof b._strict&&(a._strict=b._strict),"undefined"!=typeof b._tzm&&(a._tzm=b._tzm),"undefined"!=typeof b._isUTC&&(a._isUTC=b._isUTC),"undefined"!=typeof b._offset&&(a._offset=b._offset),"undefined"!=typeof b._pf&&(a._pf=b._pf),"undefined"!=typeof b._locale&&(a._locale=b._locale),Kb.length>0)for(c in Kb)d=Kb[c],e=b[d],"undefined"!=typeof e&&(a[d]=e);return a}function q(a){return 0>a?Math.ceil(a):Math.floor(a)}function r(a,b,c){for(var d=""+Math.abs(a),e=a>=0;d.lengthd;d++)(c&&a[d]!==b[d]||!c&&C(a[d])!==C(b[d]))&&g++;return g+f}function z(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=lc[a]||mc[b]||b}return a}function A(a){var b,d,e={};for(d in a)c(a,d)&&(b=z(d),b&&(e[b]=a[d]));return e}function B(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}vb[b]=function(e,f){var g,h,i=vb._locale[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=vb().utc().set(d,a);return i.call(vb._locale,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function C(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function D(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function E(a,b,c){return jb(vb([a,11,31+b-c]),b,c).week}function F(a){return G(a)?366:365}function G(a){return a%4===0&&a%100!==0||a%400===0}function H(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[Db]<0||a._a[Db]>11?Db:a._a[Eb]<1||a._a[Eb]>D(a._a[Cb],a._a[Db])?Eb:a._a[Fb]<0||a._a[Fb]>24||24===a._a[Fb]&&(0!==a._a[Gb]||0!==a._a[Hb]||0!==a._a[Ib])?Fb:a._a[Gb]<0||a._a[Gb]>59?Gb:a._a[Hb]<0||a._a[Hb]>59?Hb:a._a[Ib]<0||a._a[Ib]>999?Ib:-1,a._pf._overflowDayOfYear&&(Cb>b||b>Eb)&&(b=Eb),a._pf.overflow=b)}function I(b){return null==b._isValid&&(b._isValid=!isNaN(b._d.getTime())&&b._pf.overflow<0&&!b._pf.empty&&!b._pf.invalidMonth&&!b._pf.nullInput&&!b._pf.invalidFormat&&!b._pf.userInvalidated,b._strict&&(b._isValid=b._isValid&&0===b._pf.charsLeftOver&&0===b._pf.unusedTokens.length&&b._pf.bigHour===a)),b._isValid}function J(a){return a?a.toLowerCase().replace("_","-"):a}function K(a){for(var b,c,d,e,f=0;f0;){if(d=L(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&y(e,c,!0)>=b-1)break;b--}f++}return null}function L(a){var b=null;if(!Jb[a]&&Lb)try{b=vb.locale(),require("./locale/"+a),vb.locale(b)}catch(c){}return Jb[a]}function M(a,b){var c,d;return b._isUTC?(c=b.clone(),d=(vb.isMoment(a)||x(a)?+a:+vb(a))-+c,c._d.setTime(+c._d+d),vb.updateOffset(c,!1),c):vb(a).local()}function N(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function O(a){var b,c,d=a.match(Pb);for(b=0,c=d.length;c>b;b++)d[b]=rc[d[b]]?rc[d[b]]:N(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function P(a,b){return a.isValid()?(b=Q(b,a.localeData()),nc[b]||(nc[b]=O(b)),nc[b](a)):a.localeData().invalidDate()}function Q(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Qb.lastIndex=0;d>=0&&Qb.test(a);)a=a.replace(Qb,c),Qb.lastIndex=0,d-=1;return a}function R(a,b){var c,d=b._strict;switch(a){case"Q":return _b;case"DDDD":return bc;case"YYYY":case"GGGG":case"gggg":return d?cc:Tb;case"Y":case"G":case"g":return ec;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return d?dc:Ub;case"S":if(d)return _b;case"SS":if(d)return ac;case"SSS":if(d)return bc;case"DDD":return Sb;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Wb;case"a":case"A":return b._locale._meridiemParse;case"x":return Zb;case"X":return $b;case"Z":case"ZZ":return Xb;case"T":return Yb;case"SSSS":return Vb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return d?ac:Rb;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Rb;case"Do":return d?b._locale._ordinalParse:b._locale._ordinalParseLenient;default:return c=new RegExp($(Z(a.replace("\\","")),"i"))}}function S(a){a=a||"";var b=a.match(Xb)||[],c=b[b.length-1]||[],d=(c+"").match(jc)||["-",0,0],e=+(60*d[1])+C(d[2]);return"+"===d[0]?e:-e}function T(a,b,c){var d,e=c._a;switch(a){case"Q":null!=b&&(e[Db]=3*(C(b)-1));break;case"M":case"MM":null!=b&&(e[Db]=C(b)-1);break;case"MMM":case"MMMM":d=c._locale.monthsParse(b,a,c._strict),null!=d?e[Db]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[Eb]=C(b));break;case"Do":null!=b&&(e[Eb]=C(parseInt(b.match(/\d{1,2}/)[0],10)));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=C(b));break;case"YY":e[Cb]=vb.parseTwoDigitYear(b);break;case"YYYY":case"YYYYY":case"YYYYYY":e[Cb]=C(b);break;case"a":case"A":c._meridiem=b;break;case"h":case"hh":c._pf.bigHour=!0;case"H":case"HH":e[Fb]=C(b);break;case"m":case"mm":e[Gb]=C(b);break;case"s":case"ss":e[Hb]=C(b);break;case"S":case"SS":case"SSS":case"SSSS":e[Ib]=C(1e3*("0."+b));break;case"x":c._d=new Date(C(b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=S(b);break;case"dd":case"ddd":case"dddd":d=c._locale.weekdaysParse(b),null!=d?(c._w=c._w||{},c._w.d=d):c._pf.invalidWeekday=b;break;case"w":case"ww":case"W":case"WW":case"d":case"e":case"E":a=a.substr(0,1);case"gggg":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=C(b));break;case"gg":case"GG":c._w=c._w||{},c._w[a]=vb.parseTwoDigitYear(b)}}function U(a){var c,d,e,f,g,h,i;c=a._w,null!=c.GG||null!=c.W||null!=c.E?(g=1,h=4,d=b(c.GG,a._a[Cb],jb(vb(),1,4).year),e=b(c.W,1),f=b(c.E,1)):(g=a._locale._week.dow,h=a._locale._week.doy,d=b(c.gg,a._a[Cb],jb(vb(),g,h).year),e=b(c.w,1),null!=c.d?(f=c.d,g>f&&++e):f=null!=c.e?c.e+g:g),i=kb(d,e,f,h,g),a._a[Cb]=i.year,a._dayOfYear=i.dayOfYear}function V(a){var c,d,e,f,g=[];if(!a._d){for(e=X(a),a._w&&null==a._a[Eb]&&null==a._a[Db]&&U(a),a._dayOfYear&&(f=b(a._a[Cb],e[Cb]),a._dayOfYear>F(f)&&(a._pf._overflowDayOfYear=!0),d=fb(f,0,a._dayOfYear),a._a[Db]=d.getUTCMonth(),a._a[Eb]=d.getUTCDate()),c=0;3>c&&null==a._a[c];++c)a._a[c]=g[c]=e[c];for(;7>c;c++)a._a[c]=g[c]=null==a._a[c]?2===c?1:0:a._a[c];24===a._a[Fb]&&0===a._a[Gb]&&0===a._a[Hb]&&0===a._a[Ib]&&(a._nextDay=!0,a._a[Fb]=0),a._d=(a._useUTC?fb:eb).apply(null,g),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[Fb]=24)}}function W(a){var b;a._d||(b=A(a._i),a._a=[b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],V(a))}function X(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function Y(b){if(b._f===vb.ISO_8601)return void ab(b);b._a=[],b._pf.empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,j=0;for(e=Q(b._f,b._locale).match(Pb)||[],c=0;c0&&b._pf.unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),j+=d.length),rc[f]?(d?b._pf.empty=!1:b._pf.unusedTokens.push(f),T(f,d,b)):b._strict&&!d&&b._pf.unusedTokens.push(f);b._pf.charsLeftOver=i-j,h.length>0&&b._pf.unusedInput.push(h),b._pf.bigHour===!0&&b._a[Fb]<=12&&(b._pf.bigHour=a),b._a[Fb]=k(b._locale,b._a[Fb],b._meridiem),V(b),H(b)}function Z(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function $(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function _(a){var b,c,e,f,g;if(0===a._f.length)return a._pf.invalidFormat=!0,void(a._d=new Date(0/0));for(f=0;fg)&&(e=g,c=b));o(a,c||b)}function ab(a){var b,c,d=a._i,e=fc.exec(d);if(e){for(a._pf.iso=!0,b=0,c=hc.length;c>b;b++)if(hc[b][1].exec(d)){a._f=hc[b][0]+(e[6]||" ");break}for(b=0,c=ic.length;c>b;b++)if(ic[b][1].exec(d)){a._f+=ic[b][0];break}d.match(Xb)&&(a._f+="Z"),Y(a)}else a._isValid=!1}function bb(a){ab(a),a._isValid===!1&&(delete a._isValid,vb.createFromInputFallback(a))}function cb(a,b){var c,d=[];for(c=0;ca&&h.setFullYear(a),h}function fb(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function gb(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function hb(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function ib(a,b,c){var d=vb.duration(a).abs(),e=Ab(d.as("s")),f=Ab(d.as("m")),g=Ab(d.as("h")),h=Ab(d.as("d")),i=Ab(d.as("M")),j=Ab(d.as("y")),k=e0,k[4]=c,hb.apply({},k)}function jb(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=vb(a).add(f,"d"),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function kb(a,b,c,d,e){var f,g,h=fb(a,0,1).getUTCDay();return h=0===h?7:h,c=null!=c?c:e,f=e-h+(h>d?7:0)-(e>h?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:F(a-1)+g}}function lb(b){var c,d=b._i,e=b._f;return b._locale=b._locale||vb.localeData(b._l),null===d||e===a&&""===d?vb.invalid({nullInput:!0}):("string"==typeof d&&(b._i=d=b._locale.preparse(d)),vb.isMoment(d)?new m(d,!0):(e?w(e)?_(b):Y(b):db(b),c=new m(b),c._nextDay&&(c.add(1,"d"),c._nextDay=a),c))}function mb(a,b){var c,d;if(1===b.length&&w(b[0])&&(b=b[0]),!b.length)return vb();for(c=b[0],d=1;d=0?"+":"-";return b+r(Math.abs(a),6)},gg:function(){return r(this.weekYear()%100,2)},gggg:function(){return r(this.weekYear(),4)},ggggg:function(){return r(this.weekYear(),5)},GG:function(){return r(this.isoWeekYear()%100,2)},GGGG:function(){return r(this.isoWeekYear(),4)},GGGGG:function(){return r(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return C(this.milliseconds()/100)},SS:function(){return r(C(this.milliseconds()/10),2)},SSS:function(){return r(this.milliseconds(),3)},SSSS:function(){return r(this.milliseconds(),3)},Z:function(){var a=this.utcOffset(),b="+";return 0>a&&(a=-a,b="-"),b+r(C(a/60),2)+":"+r(C(a)%60,2)},ZZ:function(){var a=this.utcOffset(),b="+";return 0>a&&(a=-a,b="-"),b+r(C(a/60),2)+r(C(a)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},x:function(){return this.valueOf()},X:function(){return this.unix()},Q:function(){return this.quarter()}},sc={},tc=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"],uc=!1;pc.length;)xb=pc.pop(),rc[xb+"o"]=i(rc[xb],xb);for(;qc.length;)xb=qc.pop(),rc[xb+xb]=h(rc[xb],2);rc.DDDD=h(rc.DDD,3),o(l.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a,b,c){var d,e,f;for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;12>d;d++){if(e=vb.utc([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=vb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY LT",LLLL:"dddd, MMMM D, YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b,c){var d=this._calendar[a];return"function"==typeof d?d.apply(b,[c]):d},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",_ordinalParse:/\d{1,2}/,preparse:function(a){return a},postformat:function(a){return a},week:function(a){return jb(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},firstDayOfWeek:function(){return this._week.dow},firstDayOfYear:function(){return this._week.doy},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),vb=function(b,c,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._i=b,g._f=c,g._l=e,g._strict=f,g._isUTC=!1,g._pf=d(),lb(g)},vb.suppressDeprecationWarnings=!1,vb.createFromInputFallback=f("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}),vb.min=function(){var a=[].slice.call(arguments,0);return mb("isBefore",a)},vb.max=function(){var a=[].slice.call(arguments,0);return mb("isAfter",a)},vb.utc=function(b,c,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._useUTC=!0,g._isUTC=!0,g._l=e,g._i=b,g._f=c,g._strict=f,g._pf=d(),lb(g).utc()},vb.unix=function(a){return vb(1e3*a)},vb.duration=function(a,b){var d,e,f,g,h=a,i=null;return vb.isDuration(a)?h={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(h={},b?h[b]=a:h.milliseconds=a):(i=Nb.exec(a))?(d="-"===i[1]?-1:1,h={y:0,d:C(i[Eb])*d,h:C(i[Fb])*d,m:C(i[Gb])*d,s:C(i[Hb])*d,ms:C(i[Ib])*d}):(i=Ob.exec(a))?(d="-"===i[1]?-1:1,f=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*d},h={y:f(i[2]),M:f(i[3]),d:f(i[4]),h:f(i[5]),m:f(i[6]),s:f(i[7]),w:f(i[8])}):null==h?h={}:"object"==typeof h&&("from"in h||"to"in h)&&(g=t(vb(h.from),vb(h.to)),h={},h.ms=g.milliseconds,h.M=g.months),e=new n(h),vb.isDuration(a)&&c(a,"_locale")&&(e._locale=a._locale),e},vb.version=yb,vb.defaultFormat=gc,vb.ISO_8601=function(){},vb.momentProperties=Kb,vb.updateOffset=function(){},vb.relativeTimeThreshold=function(b,c){return oc[b]===a?!1:c===a?oc[b]:(oc[b]=c,!0)},vb.lang=f("moment.lang is deprecated. Use moment.locale instead.",function(a,b){return vb.locale(a,b)}),vb.locale=function(a,b){var c;return a&&(c="undefined"!=typeof b?vb.defineLocale(a,b):vb.localeData(a),c&&(vb.duration._locale=vb._locale=c)),vb._locale._abbr},vb.defineLocale=function(a,b){return null!==b?(b.abbr=a,Jb[a]||(Jb[a]=new l),Jb[a].set(b),vb.locale(a),Jb[a]):(delete Jb[a],null)},vb.langData=f("moment.langData is deprecated. Use moment.localeData instead.",function(a){return vb.localeData(a)}),vb.localeData=function(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return vb._locale;if(!w(a)){if(b=L(a))return b;a=[a]}return K(a)},vb.isMoment=function(a){return a instanceof m||null!=a&&c(a,"_isAMomentObject")},vb.isDuration=function(a){return a instanceof n};for(xb=tc.length-1;xb>=0;--xb)B(tc[xb]);vb.normalizeUnits=function(a){return z(a)},vb.invalid=function(a){var b=vb.utc(0/0);return null!=a?o(b._pf,a):b._pf.userInvalidated=!0,b},vb.parseZone=function(){return vb.apply(null,arguments).parseZone()},vb.parseTwoDigitYear=function(a){return C(a)+(C(a)>68?1900:2e3)},vb.isDate=x,o(vb.fn=m.prototype,{clone:function(){return vb(this)},valueOf:function(){return+this._d-6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var a=vb(this).utc();return 00:!1},parsingFlags:function(){return o({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(a){return this.utcOffset(0,a)},local:function(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(this._dateUtcOffset(),"m")),this},format:function(a){var b=P(this,a||vb.defaultFormat);return this.localeData().postformat(b)},add:u(1,"add"),subtract:u(-1,"subtract"),diff:function(a,b,c){var d,e,f=M(a,this),g=6e4*(f.utcOffset()-this.utcOffset());return b=z(b),"year"===b||"month"===b||"quarter"===b?(e=j(this,f),"quarter"===b?e/=3:"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:q(e)},from:function(a,b){return vb.duration({to:this,from:a}).locale(this.locale()).humanize(!b)},fromNow:function(a){return this.from(vb(),a)},calendar:function(a){var b=a||vb(),c=M(b,this).startOf("day"),d=this.diff(c,"days",!0),e=-6>d?"sameElse":-1>d?"lastWeek":0>d?"lastDay":1>d?"sameDay":2>d?"nextDay":7>d?"nextWeek":"sameElse";return this.format(this.localeData().calendar(e,this,vb(b)))},isLeapYear:function(){return G(this.year())},isDST:function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},day:function(a){var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=gb(a,this.localeData()),this.add(a-b,"d")):b},month:qb("Month",!0),startOf:function(a){switch(a=z(a)){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a?this.weekday(0):"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this},endOf:function(b){return b=z(b),b===a||"millisecond"===b?this:this.startOf(b).add(1,"isoWeek"===b?"week":b).subtract(1,"ms")},isAfter:function(a,b){var c;return b=z("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=vb.isMoment(a)?a:vb(a),+this>+a):(c=vb.isMoment(a)?+a:+vb(a),c<+this.clone().startOf(b))},isBefore:function(a,b){var c;return b=z("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=vb.isMoment(a)?a:vb(a),+a>+this):(c=vb.isMoment(a)?+a:+vb(a),+this.clone().endOf(b)a?this:a}),max:f("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(a){return a=vb.apply(null,arguments),a>this?this:a}),zone:f("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",function(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}),utcOffset:function(a,b){var c,d=this._offset||0;return null!=a?("string"==typeof a&&(a=S(a)),Math.abs(a)<16&&(a=60*a),!this._isUTC&&b&&(c=this._dateUtcOffset()),this._offset=a,this._isUTC=!0,null!=c&&this.add(c,"m"),d!==a&&(!b||this._changeInProgress?v(this,vb.duration(a-d,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,vb.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?d:this._dateUtcOffset()},isLocal:function(){return!this._isUTC},isUtcOffset:function(){return this._isUTC},isUtc:function(){return this._isUTC&&0===this._offset},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(S(this._i)),this},hasAlignedHourOffset:function(a){return a=a?vb(a).utcOffset():0,(this.utcOffset()-a)%60===0},daysInMonth:function(){return D(this.year(),this.month())},dayOfYear:function(a){var b=Ab((vb(this).startOf("day")-vb(this).startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")},quarter:function(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)},weekYear:function(a){var b=jb(this,this.localeData()._week.dow,this.localeData()._week.doy).year;return null==a?b:this.add(a-b,"y")},isoWeekYear:function(a){var b=jb(this,1,4).year;return null==a?b:this.add(a-b,"y")},week:function(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")},isoWeek:function(a){var b=jb(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")},weekday:function(a){var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},isoWeeksInYear:function(){return E(this.year(),1,4)},weeksInYear:function(){var a=this.localeData()._week;return E(this.year(),a.dow,a.doy)},get:function(a){return a=z(a),this[a]()},set:function(a,b){var c;if("object"==typeof a)for(c in a)this.set(c,a[c]);else a=z(a),"function"==typeof this[a]&&this[a](b);return this},locale:function(b){var c;return b===a?this._locale._abbr:(c=vb.localeData(b),null!=c&&(this._locale=c),this)},lang:f("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(b){return b===a?this.localeData():this.locale(b)}),localeData:function(){return this._locale},_dateUtcOffset:function(){return 15*-Math.round(this._d.getTimezoneOffset()/15)}}),vb.fn.millisecond=vb.fn.milliseconds=qb("Milliseconds",!1),vb.fn.second=vb.fn.seconds=qb("Seconds",!1),vb.fn.minute=vb.fn.minutes=qb("Minutes",!1),vb.fn.hour=vb.fn.hours=qb("Hours",!0),vb.fn.date=qb("Date",!0),vb.fn.dates=f("dates accessor is deprecated. Use date instead.",qb("Date",!0)),vb.fn.year=qb("FullYear",!0),vb.fn.years=f("years accessor is deprecated. Use year instead.",qb("FullYear",!0)),vb.fn.days=vb.fn.day,vb.fn.months=vb.fn.month,vb.fn.weeks=vb.fn.week,vb.fn.isoWeeks=vb.fn.isoWeek,vb.fn.quarters=vb.fn.quarter,vb.fn.toJSON=vb.fn.toISOString,vb.fn.isUTC=vb.fn.isUtc,o(vb.duration.fn=n.prototype,{_bubble:function(){var a,b,c,d=this._milliseconds,e=this._days,f=this._months,g=this._data,h=0;g.milliseconds=d%1e3,a=q(d/1e3),g.seconds=a%60,b=q(a/60),g.minutes=b%60,c=q(b/60),g.hours=c%24,e+=q(c/24),h=q(rb(e)),e-=q(sb(h)),f+=q(e/30),e%=30,h+=q(f/12),f%=12,g.days=e,g.months=f,g.years=h},abs:function(){return this._milliseconds=Math.abs(this._milliseconds),this._days=Math.abs(this._days),this._months=Math.abs(this._months),this._data.milliseconds=Math.abs(this._data.milliseconds),this._data.seconds=Math.abs(this._data.seconds),this._data.minutes=Math.abs(this._data.minutes),this._data.hours=Math.abs(this._data.hours),this._data.months=Math.abs(this._data.months),this._data.years=Math.abs(this._data.years),this},weeks:function(){return q(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*C(this._months/12) +},humanize:function(a){var b=ib(this,!a,this.localeData());return a&&(b=this.localeData().pastFuture(+this,b)),this.localeData().postformat(b)},add:function(a,b){var c=vb.duration(a,b);return this._milliseconds+=c._milliseconds,this._days+=c._days,this._months+=c._months,this._bubble(),this},subtract:function(a,b){var c=vb.duration(a,b);return this._milliseconds-=c._milliseconds,this._days-=c._days,this._months-=c._months,this._bubble(),this},get:function(a){return a=z(a),this[a.toLowerCase()+"s"]()},as:function(a){var b,c;if(a=z(a),"month"===a||"year"===a)return b=this._days+this._milliseconds/864e5,c=this._months+12*rb(b),"month"===a?c:c/12;switch(b=this._days+Math.round(sb(this._months/12)),a){case"week":return b/7+this._milliseconds/6048e5;case"day":return b+this._milliseconds/864e5;case"hour":return 24*b+this._milliseconds/36e5;case"minute":return 24*b*60+this._milliseconds/6e4;case"second":return 24*b*60*60+this._milliseconds/1e3;case"millisecond":return Math.floor(24*b*60*60*1e3)+this._milliseconds;default:throw new Error("Unknown unit "+a)}},lang:vb.fn.lang,locale:vb.fn.locale,toIsoString:f("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",function(){return this.toISOString()}),toISOString:function(){var a=Math.abs(this.years()),b=Math.abs(this.months()),c=Math.abs(this.days()),d=Math.abs(this.hours()),e=Math.abs(this.minutes()),f=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"},localeData:function(){return this._locale},toJSON:function(){return this.toISOString()}}),vb.duration.fn.toString=vb.duration.fn.toISOString;for(xb in kc)c(kc,xb)&&tb(xb.toLowerCase());vb.duration.fn.asMilliseconds=function(){return this.as("ms")},vb.duration.fn.asSeconds=function(){return this.as("s")},vb.duration.fn.asMinutes=function(){return this.as("m")},vb.duration.fn.asHours=function(){return this.as("h")},vb.duration.fn.asDays=function(){return this.as("d")},vb.duration.fn.asWeeks=function(){return this.as("weeks")},vb.duration.fn.asMonths=function(){return this.as("M")},vb.duration.fn.asYears=function(){return this.as("y")},vb.locale("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===C(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),function(a){a(vb)}(function(a){return a.defineLocale("af",{months:"Januarie_Februarie_Maart_April_Mei_Junie_Julie_Augustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Aug_Sep_Okt_Nov_Des".split("_"),weekdays:"Sondag_Maandag_Dinsdag_Woensdag_Donderdag_Vrydag_Saterdag".split("_"),weekdaysShort:"Son_Maa_Din_Woe_Don_Vry_Sat".split("_"),weekdaysMin:"So_Ma_Di_Wo_Do_Vr_Sa".split("_"),meridiemParse:/vm|nm/i,isPM:function(a){return/^nm$/i.test(a)},meridiem:function(a,b,c){return 12>a?c?"vm":"VM":c?"nm":"NM"},longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Vandag om] LT",nextDay:"[Môre om] LT",nextWeek:"dddd [om] LT",lastDay:"[Gister om] LT",lastWeek:"[Laas] dddd [om] LT",sameElse:"L"},relativeTime:{future:"oor %s",past:"%s gelede",s:"'n paar sekondes",m:"'n minuut",mm:"%d minute",h:"'n uur",hh:"%d ure",d:"'n dag",dd:"%d dae",M:"'n maand",MM:"%d maande",y:"'n jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(a){return a+(1===a||8===a||a>=20?"ste":"de")},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("ar-ma",{months:"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر".split("_"),monthsShort:"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر".split("_"),weekdays:"الأحد_الإتنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"احد_اتنين_ثلاثاء_اربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},week:{dow:6,doy:12}})}),function(a){a(vb)}(function(a){var b={1:"١",2:"٢",3:"٣",4:"٤",5:"٥",6:"٦",7:"٧",8:"٨",9:"٩",0:"٠"},c={"١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","٠":"0"};return a.defineLocale("ar-sa",{months:"يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),monthsShort:"يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},meridiemParse:/ص|م/,isPM:function(a){return"م"===a},meridiem:function(a){return 12>a?"ص":"م"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},preparse:function(a){return a.replace(/[١٢٣٤٥٦٧٨٩٠]/g,function(a){return c[a]}).replace(/،/g,",")},postformat:function(a){return a.replace(/\d/g,function(a){return b[a]}).replace(/,/g,"،")},week:{dow:6,doy:12}})}),function(a){a(vb)}(function(a){return a.defineLocale("ar-tn",{months:"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),monthsShort:"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){var b={1:"١",2:"٢",3:"٣",4:"٤",5:"٥",6:"٦",7:"٧",8:"٨",9:"٩",0:"٠"},c={"١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","٠":"0"},d=function(a){return 0===a?0:1===a?1:2===a?2:a%100>=3&&10>=a%100?3:a%100>=11?4:5},e={s:["أقل من ثانية","ثانية واحدة",["ثانيتان","ثانيتين"],"%d ثوان","%d ثانية","%d ثانية"],m:["أقل من دقيقة","دقيقة واحدة",["دقيقتان","دقيقتين"],"%d دقائق","%d دقيقة","%d دقيقة"],h:["أقل من ساعة","ساعة واحدة",["ساعتان","ساعتين"],"%d ساعات","%d ساعة","%d ساعة"],d:["أقل من يوم","يوم واحد",["يومان","يومين"],"%d أيام","%d يومًا","%d يوم"],M:["أقل من شهر","شهر واحد",["شهران","شهرين"],"%d أشهر","%d شهرا","%d شهر"],y:["أقل من عام","عام واحد",["عامان","عامين"],"%d أعوام","%d عامًا","%d عام"]},f=function(a){return function(b,c){var f=d(b),g=e[a][d(b)];return 2===f&&(g=g[c?0:1]),g.replace(/%d/i,b)}},g=["كانون الثاني يناير","شباط فبراير","آذار مارس","نيسان أبريل","أيار مايو","حزيران يونيو","تموز يوليو","آب أغسطس","أيلول سبتمبر","تشرين الأول أكتوبر","تشرين الثاني نوفمبر","كانون الأول ديسمبر"];return a.defineLocale("ar",{months:g,monthsShort:g,weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},meridiemParse:/ص|م/,isPM:function(a){return"م"===a},meridiem:function(a){return 12>a?"ص":"م"},calendar:{sameDay:"[اليوم عند الساعة] LT",nextDay:"[غدًا عند الساعة] LT",nextWeek:"dddd [عند الساعة] LT",lastDay:"[أمس عند الساعة] LT",lastWeek:"dddd [عند الساعة] LT",sameElse:"L"},relativeTime:{future:"بعد %s",past:"منذ %s",s:f("s"),m:f("m"),mm:f("m"),h:f("h"),hh:f("h"),d:f("d"),dd:f("d"),M:f("M"),MM:f("M"),y:f("y"),yy:f("y")},preparse:function(a){return a.replace(/[١٢٣٤٥٦٧٨٩٠]/g,function(a){return c[a]}).replace(/،/g,",")},postformat:function(a){return a.replace(/\d/g,function(a){return b[a]}).replace(/,/g,"،")},week:{dow:6,doy:12}})}),function(a){a(vb)}(function(a){var b={1:"-inci",5:"-inci",8:"-inci",70:"-inci",80:"-inci",2:"-nci",7:"-nci",20:"-nci",50:"-nci",3:"-üncü",4:"-üncü",100:"-üncü",6:"-ncı",9:"-uncu",10:"-uncu",30:"-uncu",60:"-ıncı",90:"-ıncı"};return a.defineLocale("az",{months:"yanvar_fevral_mart_aprel_may_iyun_iyul_avqust_sentyabr_oktyabr_noyabr_dekabr".split("_"),monthsShort:"yan_fev_mar_apr_may_iyn_iyl_avq_sen_okt_noy_dek".split("_"),weekdays:"Bazar_Bazar ertəsi_Çərşənbə axşamı_Çərşənbə_Cümə axşamı_Cümə_Şənbə".split("_"),weekdaysShort:"Baz_BzE_ÇAx_Çər_CAx_Cüm_Şən".split("_"),weekdaysMin:"Bz_BE_ÇA_Çə_CA_Cü_Şə".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[bugün saat] LT",nextDay:"[sabah saat] LT",nextWeek:"[gələn həftə] dddd [saat] LT",lastDay:"[dünən] LT",lastWeek:"[keçən həftə] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s əvvəl",s:"birneçə saniyyə",m:"bir dəqiqə",mm:"%d dəqiqə",h:"bir saat",hh:"%d saat",d:"bir gün",dd:"%d gün",M:"bir ay",MM:"%d ay",y:"bir il",yy:"%d il"},meridiemParse:/gecə|səhər|gündüz|axşam/,isPM:function(a){return/^(gündüz|axşam)$/.test(a)},meridiem:function(a){return 4>a?"gecə":12>a?"səhər":17>a?"gündüz":"axşam"},ordinalParse:/\d{1,2}-(ıncı|inci|nci|üncü|ncı|uncu)/,ordinal:function(a){if(0===a)return a+"-ıncı";var c=a%10,d=a%100-c,e=a>=100?100:null;return a+(b[c]||b[d]||b[e])},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){function b(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function c(a,c,d){var e={mm:c?"хвіліна_хвіліны_хвілін":"хвіліну_хвіліны_хвілін",hh:c?"гадзіна_гадзіны_гадзін":"гадзіну_гадзіны_гадзін",dd:"дзень_дні_дзён",MM:"месяц_месяцы_месяцаў",yy:"год_гады_гадоў"};return"m"===d?c?"хвіліна":"хвіліну":"h"===d?c?"гадзіна":"гадзіну":a+" "+b(e[d],+a)}function d(a,b){var c={nominative:"студзень_люты_сакавік_красавік_травень_чэрвень_ліпень_жнівень_верасень_кастрычнік_лістапад_снежань".split("_"),accusative:"студзеня_лютага_сакавіка_красавіка_траўня_чэрвеня_ліпеня_жніўня_верасня_кастрычніка_лістапада_снежня".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function e(a,b){var c={nominative:"нядзеля_панядзелак_аўторак_серада_чацвер_пятніца_субота".split("_"),accusative:"нядзелю_панядзелак_аўторак_сераду_чацвер_пятніцу_суботу".split("_")},d=/\[ ?[Вв] ?(?:мінулую|наступную)? ?\] ?dddd/.test(b)?"accusative":"nominative";return c[d][a.day()]}return a.defineLocale("be",{months:d,monthsShort:"студ_лют_сак_крас_трав_чэрв_ліп_жнів_вер_каст_ліст_снеж".split("_"),weekdays:e,weekdaysShort:"нд_пн_ат_ср_чц_пт_сб".split("_"),weekdaysMin:"нд_пн_ат_ср_чц_пт_сб".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY г.",LLL:"D MMMM YYYY г., LT",LLLL:"dddd, D MMMM YYYY г., LT"},calendar:{sameDay:"[Сёння ў] LT",nextDay:"[Заўтра ў] LT",lastDay:"[Учора ў] LT",nextWeek:function(){return"[У] dddd [ў] LT"},lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return"[У мінулую] dddd [ў] LT";case 1:case 2:case 4:return"[У мінулы] dddd [ў] LT"}},sameElse:"L"},relativeTime:{future:"праз %s",past:"%s таму",s:"некалькі секунд",m:c,mm:c,h:c,hh:c,d:"дзень",dd:c,M:"месяц",MM:c,y:"год",yy:c},meridiemParse:/ночы|раніцы|дня|вечара/,isPM:function(a){return/^(дня|вечара)$/.test(a)},meridiem:function(a){return 4>a?"ночы":12>a?"раніцы":17>a?"дня":"вечара"},ordinalParse:/\d{1,2}-(і|ы|га)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":case"w":case"W":return a%10!==2&&a%10!==3||a%100===12||a%100===13?a+"-ы":a+"-і";case"D":return a+"-га";default:return a}},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){return a.defineLocale("bg",{months:"януари_февруари_март_април_май_юни_юли_август_септември_октомври_ноември_декември".split("_"),monthsShort:"янр_фев_мар_апр_май_юни_юли_авг_сеп_окт_ное_дек".split("_"),weekdays:"неделя_понеделник_вторник_сряда_четвъртък_петък_събота".split("_"),weekdaysShort:"нед_пон_вто_сря_чет_пет_съб".split("_"),weekdaysMin:"нд_пн_вт_ср_чт_пт_сб".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"D.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Днес в] LT",nextDay:"[Утре в] LT",nextWeek:"dddd [в] LT",lastDay:"[Вчера в] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[В изминалата] dddd [в] LT";case 1:case 2:case 4:case 5:return"[В изминалия] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"след %s",past:"преди %s",s:"няколко секунди",m:"минута",mm:"%d минути",h:"час",hh:"%d часа",d:"ден",dd:"%d дни",M:"месец",MM:"%d месеца",y:"година",yy:"%d години"},ordinalParse:/\d{1,2}-(ев|ен|ти|ви|ри|ми)/,ordinal:function(a){var b=a%10,c=a%100;return 0===a?a+"-ев":0===c?a+"-ен":c>10&&20>c?a+"-ти":1===b?a+"-ви":2===b?a+"-ри":7===b||8===b?a+"-ми":a+"-ти"},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){var b={1:"১",2:"২",3:"৩",4:"৪",5:"৫",6:"৬",7:"৭",8:"৮",9:"৯",0:"০"},c={"১":"1","২":"2","৩":"3","৪":"4","৫":"5","৬":"6","৭":"7","৮":"8","৯":"9","০":"0"};return a.defineLocale("bn",{months:"জানুয়ারী_ফেবুয়ারী_মার্চ_এপ্রিল_মে_জুন_জুলাই_অগাস্ট_সেপ্টেম্বর_অক্টোবর_নভেম্বর_ডিসেম্বর".split("_"),monthsShort:"জানু_ফেব_মার্চ_এপর_মে_জুন_জুল_অগ_সেপ্ট_অক্টো_নভ_ডিসেম্".split("_"),weekdays:"রবিবার_সোমবার_মঙ্গলবার_বুধবার_বৃহস্পত্তিবার_শুক্রুবার_শনিবার".split("_"),weekdaysShort:"রবি_সোম_মঙ্গল_বুধ_বৃহস্পত্তি_শুক্রু_শনি".split("_"),weekdaysMin:"রব_সম_মঙ্গ_বু_ব্রিহ_শু_শনি".split("_"),longDateFormat:{LT:"A h:mm সময়",LTS:"A h:mm:ss সময়",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, LT",LLLL:"dddd, D MMMM YYYY, LT"},calendar:{sameDay:"[আজ] LT",nextDay:"[আগামীকাল] LT",nextWeek:"dddd, LT",lastDay:"[গতকাল] LT",lastWeek:"[গত] dddd, LT",sameElse:"L"},relativeTime:{future:"%s পরে",past:"%s আগে",s:"কএক সেকেন্ড",m:"এক মিনিট",mm:"%d মিনিট",h:"এক ঘন্টা",hh:"%d ঘন্টা",d:"এক দিন",dd:"%d দিন",M:"এক মাস",MM:"%d মাস",y:"এক বছর",yy:"%d বছর"},preparse:function(a){return a.replace(/[১২৩৪৫৬৭৮৯০]/g,function(a){return c[a]})},postformat:function(a){return a.replace(/\d/g,function(a){return b[a]})},meridiemParse:/রাত|শকাল|দুপুর|বিকেল|রাত/,isPM:function(a){return/^(দুপুর|বিকেল|রাত)$/.test(a)},meridiem:function(a){return 4>a?"রাত":10>a?"শকাল":17>a?"দুপুর":20>a?"বিকেল":"রাত"},week:{dow:0,doy:6}})}),function(a){a(vb)}(function(a){var b={1:"༡",2:"༢",3:"༣",4:"༤",5:"༥",6:"༦",7:"༧",8:"༨",9:"༩",0:"༠"},c={"༡":"1","༢":"2","༣":"3","༤":"4","༥":"5","༦":"6","༧":"7","༨":"8","༩":"9","༠":"0"};return a.defineLocale("bo",{months:"ཟླ་བ་དང་པོ_ཟླ་བ་གཉིས་པ_ཟླ་བ་གསུམ་པ_ཟླ་བ་བཞི་པ_ཟླ་བ་ལྔ་པ_ཟླ་བ་དྲུག་པ_ཟླ་བ་བདུན་པ_ཟླ་བ་བརྒྱད་པ_ཟླ་བ་དགུ་པ_ཟླ་བ་བཅུ་པ_ཟླ་བ་བཅུ་གཅིག་པ_ཟླ་བ་བཅུ་གཉིས་པ".split("_"),monthsShort:"ཟླ་བ་དང་པོ_ཟླ་བ་གཉིས་པ_ཟླ་བ་གསུམ་པ_ཟླ་བ་བཞི་པ_ཟླ་བ་ལྔ་པ_ཟླ་བ་དྲུག་པ_ཟླ་བ་བདུན་པ_ཟླ་བ་བརྒྱད་པ_ཟླ་བ་དགུ་པ_ཟླ་བ་བཅུ་པ_ཟླ་བ་བཅུ་གཅིག་པ_ཟླ་བ་བཅུ་གཉིས་པ".split("_"),weekdays:"གཟའ་ཉི་མ་_གཟའ་ཟླ་བ་_གཟའ་མིག་དམར་_གཟའ་ལྷག་པ་_གཟའ་ཕུར་བུ_གཟའ་པ་སངས་_གཟའ་སྤེན་པ་".split("_"),weekdaysShort:"ཉི་མ་_ཟླ་བ་_མིག་དམར་_ལྷག་པ་_ཕུར་བུ_པ་སངས་_སྤེན་པ་".split("_"),weekdaysMin:"ཉི་མ་_ཟླ་བ་_མིག་དམར་_ལྷག་པ་_ཕུར་བུ_པ་སངས་_སྤེན་པ་".split("_"),longDateFormat:{LT:"A h:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, LT",LLLL:"dddd, D MMMM YYYY, LT"},calendar:{sameDay:"[དི་རིང] LT",nextDay:"[སང་ཉིན] LT",nextWeek:"[བདུན་ཕྲག་རྗེས་མ], LT",lastDay:"[ཁ་སང] LT",lastWeek:"[བདུན་ཕྲག་མཐའ་མ] dddd, LT",sameElse:"L"},relativeTime:{future:"%s ལ་",past:"%s སྔན་ལ",s:"ལམ་སང",m:"སྐར་མ་གཅིག",mm:"%d སྐར་མ",h:"ཆུ་ཚོད་གཅིག",hh:"%d ཆུ་ཚོད",d:"ཉིན་གཅིག",dd:"%d ཉིན་",M:"ཟླ་བ་གཅིག",MM:"%d ཟླ་བ",y:"ལོ་གཅིག",yy:"%d ལོ"},preparse:function(a){return a.replace(/[༡༢༣༤༥༦༧༨༩༠]/g,function(a){return c[a]})},postformat:function(a){return a.replace(/\d/g,function(a){return b[a]})},meridiemParse:/མཚན་མོ|ཞོགས་ཀས|ཉིན་གུང|དགོང་དག|མཚན་མོ/,isPM:function(a){return/^(ཉིན་གུང|དགོང་དག|མཚན་མོ)$/.test(a)},meridiem:function(a){return 4>a?"མཚན་མོ":10>a?"ཞོགས་ཀས":17>a?"ཉིན་གུང":20>a?"དགོང་དག":"མཚན་མོ"},week:{dow:0,doy:6}})}),function(a){a(vb)}(function(b){function c(a,b,c){var d={mm:"munutenn",MM:"miz",dd:"devezh"};return a+" "+f(d[c],a)}function d(a){switch(e(a)){case 1:case 3:case 4:case 5:case 9:return a+" bloaz";default:return a+" vloaz"}}function e(a){return a>9?e(a%10):a}function f(a,b){return 2===b?g(a):a}function g(b){var c={m:"v",b:"v",d:"z"};return c[b.charAt(0)]===a?b:c[b.charAt(0)]+b.substring(1)}return b.defineLocale("br",{months:"Genver_C'hwevrer_Meurzh_Ebrel_Mae_Mezheven_Gouere_Eost_Gwengolo_Here_Du_Kerzu".split("_"),monthsShort:"Gen_C'hwe_Meu_Ebr_Mae_Eve_Gou_Eos_Gwe_Her_Du_Ker".split("_"),weekdays:"Sul_Lun_Meurzh_Merc'her_Yaou_Gwener_Sadorn".split("_"),weekdaysShort:"Sul_Lun_Meu_Mer_Yao_Gwe_Sad".split("_"),weekdaysMin:"Su_Lu_Me_Mer_Ya_Gw_Sa".split("_"),longDateFormat:{LT:"h[e]mm A",LTS:"h[e]mm:ss A",L:"DD/MM/YYYY",LL:"D [a viz] MMMM YYYY",LLL:"D [a viz] MMMM YYYY LT",LLLL:"dddd, D [a viz] MMMM YYYY LT"},calendar:{sameDay:"[Hiziv da] LT",nextDay:"[Warc'hoazh da] LT",nextWeek:"dddd [da] LT",lastDay:"[Dec'h da] LT",lastWeek:"dddd [paset da] LT",sameElse:"L"},relativeTime:{future:"a-benn %s",past:"%s 'zo",s:"un nebeud segondennoù",m:"ur vunutenn",mm:c,h:"un eur",hh:"%d eur",d:"un devezh",dd:c,M:"ur miz",MM:c,y:"ur bloaz",yy:d},ordinalParse:/\d{1,2}(añ|vet)/,ordinal:function(a){var b=1===a?"añ":"vet";return a+b},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){function b(a,b,c){var d=a+" ";switch(c){case"m":return b?"jedna minuta":"jedne minute";case"mm":return d+=1===a?"minuta":2===a||3===a||4===a?"minute":"minuta";case"h":return b?"jedan sat":"jednog sata";case"hh":return d+=1===a?"sat":2===a||3===a||4===a?"sata":"sati";case"dd":return d+=1===a?"dan":"dana";case"MM":return d+=1===a?"mjesec":2===a||3===a||4===a?"mjeseca":"mjeseci";case"yy":return d+=1===a?"godina":2===a||3===a||4===a?"godine":"godina"}}return a.defineLocale("bs",{months:"januar_februar_mart_april_maj_juni_juli_august_septembar_oktobar_novembar_decembar".split("_"),monthsShort:"jan._feb._mar._apr._maj._jun._jul._aug._sep._okt._nov._dec.".split("_"),weekdays:"nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._čet._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_če_pe_su".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[jučer u] LT",lastWeek:function(){switch(this.day()){case 0:case 3:return"[prošlu] dddd [u] LT";case 6:return"[prošle] [subote] [u] LT";case 1:case 2:case 4:case 5:return"[prošli] dddd [u] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"par sekundi",m:b,mm:b,h:b,hh:b,d:"dan",dd:b,M:"mjesec",MM:b,y:"godinu",yy:b},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){return a.defineLocale("ca",{months:"gener_febrer_març_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre".split("_"),monthsShort:"gen._febr._mar._abr._mai._jun._jul._ag._set._oct._nov._des.".split("_"),weekdays:"diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte".split("_"),weekdaysShort:"dg._dl._dt._dc._dj._dv._ds.".split("_"),weekdaysMin:"Dg_Dl_Dt_Dc_Dj_Dv_Ds".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:function(){return"[avui a "+(1!==this.hours()?"les":"la")+"] LT"},nextDay:function(){return"[demà a "+(1!==this.hours()?"les":"la")+"] LT"},nextWeek:function(){return"dddd [a "+(1!==this.hours()?"les":"la")+"] LT"},lastDay:function(){return"[ahir a "+(1!==this.hours()?"les":"la")+"] LT"},lastWeek:function(){return"[el] dddd [passat a "+(1!==this.hours()?"les":"la")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"fa %s",s:"uns segons",m:"un minut",mm:"%d minuts",h:"una hora",hh:"%d hores",d:"un dia",dd:"%d dies",M:"un mes",MM:"%d mesos",y:"un any",yy:"%d anys"},ordinalParse:/\d{1,2}(r|n|t|è|a)/,ordinal:function(a,b){var c=1===a?"r":2===a?"n":3===a?"r":4===a?"t":"è";return("w"===b||"W"===b)&&(c="a"),a+c},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){function b(a){return a>1&&5>a&&1!==~~(a/10)}function c(a,c,d,e){var f=a+" ";switch(d){case"s":return c||e?"pár sekund":"pár sekundami";case"m":return c?"minuta":e?"minutu":"minutou";case"mm":return c||e?f+(b(a)?"minuty":"minut"):f+"minutami";break;case"h":return c?"hodina":e?"hodinu":"hodinou";case"hh":return c||e?f+(b(a)?"hodiny":"hodin"):f+"hodinami";break;case"d":return c||e?"den":"dnem";case"dd":return c||e?f+(b(a)?"dny":"dní"):f+"dny";break;case"M":return c||e?"měsíc":"měsícem";case"MM":return c||e?f+(b(a)?"měsíce":"měsíců"):f+"měsíci";break;case"y":return c||e?"rok":"rokem";case"yy":return c||e?f+(b(a)?"roky":"let"):f+"lety"}}var d="leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec".split("_"),e="led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_");return a.defineLocale("cs",{months:d,monthsShort:e,monthsParse:function(a,b){var c,d=[];for(c=0;12>c;c++)d[c]=new RegExp("^"+a[c]+"$|^"+b[c]+"$","i");return d}(d,e),weekdays:"neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"),weekdaysShort:"ne_po_út_st_čt_pá_so".split("_"),weekdaysMin:"ne_po_út_st_čt_pá_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd D. MMMM YYYY LT"},calendar:{sameDay:"[dnes v] LT",nextDay:"[zítra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v neděli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve středu v] LT";case 4:return"[ve čtvrtek v] LT";case 5:return"[v pátek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[včera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou neděli v] LT";case 1:case 2:return"[minulé] dddd [v] LT";case 3:return"[minulou středu v] LT";case 4:case 5:return"[minulý] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"před %s",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("cv",{months:"кăрлач_нарăс_пуш_ака_май_çĕртме_утă_çурла_авăн_юпа_чӳк_раштав".split("_"),monthsShort:"кăр_нар_пуш_ака_май_çĕр_утă_çур_ав_юпа_чӳк_раш".split("_"),weekdays:"вырсарникун_тунтикун_ытларикун_юнкун_кĕçнерникун_эрнекун_шăматкун".split("_"),weekdaysShort:"выр_тун_ытл_юн_кĕç_эрн_шăм".split("_"),weekdaysMin:"вр_тн_ыт_юн_кç_эр_шм".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD-MM-YYYY",LL:"YYYY [çулхи] MMMM [уйăхĕн] D[-мĕшĕ]",LLL:"YYYY [çулхи] MMMM [уйăхĕн] D[-мĕшĕ], LT",LLLL:"dddd, YYYY [çулхи] MMMM [уйăхĕн] D[-мĕшĕ], LT"},calendar:{sameDay:"[Паян] LT [сехетре]",nextDay:"[Ыран] LT [сехетре]",lastDay:"[Ĕнер] LT [сехетре]",nextWeek:"[Çитес] dddd LT [сехетре]",lastWeek:"[Иртнĕ] dddd LT [сехетре]",sameElse:"L"},relativeTime:{future:function(a){var b=/сехет$/i.exec(a)?"рен":/çул$/i.exec(a)?"тан":"ран";return a+b},past:"%s каялла",s:"пĕр-ик çеккунт",m:"пĕр минут",mm:"%d минут",h:"пĕр сехет",hh:"%d сехет",d:"пĕр кун",dd:"%d кун",M:"пĕр уйăх",MM:"%d уйăх",y:"пĕр çул",yy:"%d çул"},ordinalParse:/\d{1,2}-мĕш/,ordinal:"%d-мĕш",week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){return a.defineLocale("cy",{months:"Ionawr_Chwefror_Mawrth_Ebrill_Mai_Mehefin_Gorffennaf_Awst_Medi_Hydref_Tachwedd_Rhagfyr".split("_"),monthsShort:"Ion_Chwe_Maw_Ebr_Mai_Meh_Gor_Aws_Med_Hyd_Tach_Rhag".split("_"),weekdays:"Dydd Sul_Dydd Llun_Dydd Mawrth_Dydd Mercher_Dydd Iau_Dydd Gwener_Dydd Sadwrn".split("_"),weekdaysShort:"Sul_Llun_Maw_Mer_Iau_Gwe_Sad".split("_"),weekdaysMin:"Su_Ll_Ma_Me_Ia_Gw_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Heddiw am] LT",nextDay:"[Yfory am] LT",nextWeek:"dddd [am] LT",lastDay:"[Ddoe am] LT",lastWeek:"dddd [diwethaf am] LT",sameElse:"L"},relativeTime:{future:"mewn %s",past:"%s yn ôl",s:"ychydig eiliadau",m:"munud",mm:"%d munud",h:"awr",hh:"%d awr",d:"diwrnod",dd:"%d diwrnod",M:"mis",MM:"%d mis",y:"blwyddyn",yy:"%d flynedd"},ordinalParse:/\d{1,2}(fed|ain|af|il|ydd|ed|eg)/,ordinal:function(a){var b=a,c="",d=["","af","il","ydd","ydd","ed","ed","ed","fed","fed","fed","eg","fed","eg","eg","fed","eg","eg","fed","eg","fed"];return b>20?c=40===b||50===b||60===b||80===b||100===b?"fed":"ain":b>0&&(c=d[b]),a+c},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tir_ons_tor_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd [d.] D. MMMM YYYY LT"},calendar:{sameDay:"[I dag kl.] LT",nextDay:"[I morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[I går kl.] LT",lastWeek:"[sidste] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"få sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en måned",MM:"%d måneder",y:"et år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){function b(a,b,c){var d={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?d[c][0]:d[c][1]}return a.defineLocale("de-at",{months:"Jänner_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jän._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[Heute um] LT [Uhr]",sameElse:"L",nextDay:"[Morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[Gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:b,mm:"%d Minuten",h:b,hh:"%d Stunden",d:b,dd:b,M:b,MM:b,y:b,yy:b},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){function b(a,b,c){var d={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?d[c][0]:d[c][1]}return a.defineLocale("de",{months:"Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[Heute um] LT [Uhr]",sameElse:"L",nextDay:"[Morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[Gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:b,mm:"%d Minuten",h:b,hh:"%d Stunden",d:b,dd:b,M:b,MM:b,y:b,yy:b},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("el",{monthsNominativeEl:"Ιανουάριος_Φεβρουάριος_Μάρτιος_Απρίλιος_Μάιος_Ιούνιος_Ιούλιος_Αύγουστος_Σεπτέμβριος_Οκτώβριος_Νοέμβριος_Δεκέμβριος".split("_"),monthsGenitiveEl:"Ιανουαρίου_Φεβρουαρίου_Μαρτίου_Απριλίου_Μαΐου_Ιουνίου_Ιουλίου_Αυγούστου_Σεπτεμβρίου_Οκτωβρίου_Νοεμβρίου_Δεκεμβρίου".split("_"),months:function(a,b){return/D/.test(b.substring(0,b.indexOf("MMMM")))?this._monthsGenitiveEl[a.month()]:this._monthsNominativeEl[a.month()]},monthsShort:"Ιαν_Φεβ_Μαρ_Απρ_Μαϊ_Ιουν_Ιουλ_Αυγ_Σεπ_Οκτ_Νοε_Δεκ".split("_"),weekdays:"Κυριακή_Δευτέρα_Τρίτη_Τετάρτη_Πέμπτη_Παρασκευή_Σάββατο".split("_"),weekdaysShort:"Κυρ_Δευ_Τρι_Τετ_Πεμ_Παρ_Σαβ".split("_"),weekdaysMin:"Κυ_Δε_Τρ_Τε_Πε_Πα_Σα".split("_"),meridiem:function(a,b,c){return a>11?c?"μμ":"ΜΜ":c?"πμ":"ΠΜ"},isPM:function(a){return"μ"===(a+"").toLowerCase()[0]},meridiemParse:/[ΠΜ]\.?Μ?\.?/i,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendarEl:{sameDay:"[Σήμερα {}] LT",nextDay:"[Αύριο {}] LT",nextWeek:"dddd [{}] LT",lastDay:"[Χθες {}] LT",lastWeek:function(){switch(this.day()){case 6:return"[το προηγούμενο] dddd [{}] LT";default:return"[την προηγούμενη] dddd [{}] LT"}},sameElse:"L"},calendar:function(a,b){var c=this._calendarEl[a],d=b&&b.hours();return"function"==typeof c&&(c=c.apply(b)),c.replace("{}",d%12===1?"στη":"στις")},relativeTime:{future:"σε %s",past:"%s πριν",s:"λίγα δευτερόλεπτα",m:"ένα λεπτό",mm:"%d λεπτά",h:"μία ώρα",hh:"%d ώρες",d:"μία μέρα",dd:"%d μέρες",M:"ένας μήνας",MM:"%d μήνες",y:"ένας χρόνος",yy:"%d χρόνια"},ordinalParse:/\d{1,2}η/,ordinal:"%dη",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("en-au",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("en-ca",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"YYYY-MM-DD",LL:"D MMMM, YYYY",LLL:"D MMMM, YYYY LT",LLLL:"dddd, D MMMM, YYYY LT"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th"; +return a+c}})}),function(a){a(vb)}(function(a){return a.defineLocale("en-gb",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("eo",{months:"januaro_februaro_marto_aprilo_majo_junio_julio_aŭgusto_septembro_oktobro_novembro_decembro".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aŭg_sep_okt_nov_dec".split("_"),weekdays:"Dimanĉo_Lundo_Mardo_Merkredo_Ĵaŭdo_Vendredo_Sabato".split("_"),weekdaysShort:"Dim_Lun_Mard_Merk_Ĵaŭ_Ven_Sab".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Ĵa_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"D[-an de] MMMM, YYYY",LLL:"D[-an de] MMMM, YYYY LT",LLLL:"dddd, [la] D[-an de] MMMM, YYYY LT"},meridiemParse:/[ap]\.t\.m/i,isPM:function(a){return"p"===a.charAt(0).toLowerCase()},meridiem:function(a,b,c){return a>11?c?"p.t.m.":"P.T.M.":c?"a.t.m.":"A.T.M."},calendar:{sameDay:"[Hodiaŭ je] LT",nextDay:"[Morgaŭ je] LT",nextWeek:"dddd [je] LT",lastDay:"[Hieraŭ je] LT",lastWeek:"[pasinta] dddd [je] LT",sameElse:"L"},relativeTime:{future:"je %s",past:"antaŭ %s",s:"sekundoj",m:"minuto",mm:"%d minutoj",h:"horo",hh:"%d horoj",d:"tago",dd:"%d tagoj",M:"monato",MM:"%d monatoj",y:"jaro",yy:"%d jaroj"},ordinalParse:/\d{1,2}a/,ordinal:"%da",week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){var b="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),c="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_");return a.defineLocale("es",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(a,d){return/-MMM-/.test(d)?c[a.month()]:b[a.month()]},weekdays:"domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"),weekdaysShort:"dom._lun._mar._mié._jue._vie._sáb.".split("_"),weekdaysMin:"Do_Lu_Ma_Mi_Ju_Vi_Sá".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[mañana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un año",yy:"%d años"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){function b(a,b,c,d){var e={s:["mõne sekundi","mõni sekund","paar sekundit"],m:["ühe minuti","üks minut"],mm:[a+" minuti",a+" minutit"],h:["ühe tunni","tund aega","üks tund"],hh:[a+" tunni",a+" tundi"],d:["ühe päeva","üks päev"],M:["kuu aja","kuu aega","üks kuu"],MM:[a+" kuu",a+" kuud"],y:["ühe aasta","aasta","üks aasta"],yy:[a+" aasta",a+" aastat"]};return b?e[c][2]?e[c][2]:e[c][1]:d?e[c][0]:e[c][1]}return a.defineLocale("et",{months:"jaanuar_veebruar_märts_aprill_mai_juuni_juuli_august_september_oktoober_november_detsember".split("_"),monthsShort:"jaan_veebr_märts_apr_mai_juuni_juuli_aug_sept_okt_nov_dets".split("_"),weekdays:"pühapäev_esmaspäev_teisipäev_kolmapäev_neljapäev_reede_laupäev".split("_"),weekdaysShort:"P_E_T_K_N_R_L".split("_"),weekdaysMin:"P_E_T_K_N_R_L".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[Täna,] LT",nextDay:"[Homme,] LT",nextWeek:"[Järgmine] dddd LT",lastDay:"[Eile,] LT",lastWeek:"[Eelmine] dddd LT",sameElse:"L"},relativeTime:{future:"%s pärast",past:"%s tagasi",s:b,m:b,mm:b,h:b,hh:b,d:b,dd:"%d päeva",M:b,MM:b,y:b,yy:b},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("eu",{months:"urtarrila_otsaila_martxoa_apirila_maiatza_ekaina_uztaila_abuztua_iraila_urria_azaroa_abendua".split("_"),monthsShort:"urt._ots._mar._api._mai._eka._uzt._abu._ira._urr._aza._abe.".split("_"),weekdays:"igandea_astelehena_asteartea_asteazkena_osteguna_ostirala_larunbata".split("_"),weekdaysShort:"ig._al._ar._az._og._ol._lr.".split("_"),weekdaysMin:"ig_al_ar_az_og_ol_lr".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"YYYY[ko] MMMM[ren] D[a]",LLL:"YYYY[ko] MMMM[ren] D[a] LT",LLLL:"dddd, YYYY[ko] MMMM[ren] D[a] LT",l:"YYYY-M-D",ll:"YYYY[ko] MMM D[a]",lll:"YYYY[ko] MMM D[a] LT",llll:"ddd, YYYY[ko] MMM D[a] LT"},calendar:{sameDay:"[gaur] LT[etan]",nextDay:"[bihar] LT[etan]",nextWeek:"dddd LT[etan]",lastDay:"[atzo] LT[etan]",lastWeek:"[aurreko] dddd LT[etan]",sameElse:"L"},relativeTime:{future:"%s barru",past:"duela %s",s:"segundo batzuk",m:"minutu bat",mm:"%d minutu",h:"ordu bat",hh:"%d ordu",d:"egun bat",dd:"%d egun",M:"hilabete bat",MM:"%d hilabete",y:"urte bat",yy:"%d urte"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){var b={1:"۱",2:"۲",3:"۳",4:"۴",5:"۵",6:"۶",7:"۷",8:"۸",9:"۹",0:"۰"},c={"۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","۰":"0"};return a.defineLocale("fa",{months:"ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"),monthsShort:"ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"),weekdays:"یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"),weekdaysShort:"یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"),weekdaysMin:"ی_د_س_چ_پ_ج_ش".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},meridiemParse:/قبل از ظهر|بعد از ظهر/,isPM:function(a){return/بعد از ظهر/.test(a)},meridiem:function(a){return 12>a?"قبل از ظهر":"بعد از ظهر"},calendar:{sameDay:"[امروز ساعت] LT",nextDay:"[فردا ساعت] LT",nextWeek:"dddd [ساعت] LT",lastDay:"[دیروز ساعت] LT",lastWeek:"dddd [پیش] [ساعت] LT",sameElse:"L"},relativeTime:{future:"در %s",past:"%s پیش",s:"چندین ثانیه",m:"یک دقیقه",mm:"%d دقیقه",h:"یک ساعت",hh:"%d ساعت",d:"یک روز",dd:"%d روز",M:"یک ماه",MM:"%d ماه",y:"یک سال",yy:"%d سال"},preparse:function(a){return a.replace(/[۰-۹]/g,function(a){return c[a]}).replace(/،/g,",")},postformat:function(a){return a.replace(/\d/g,function(a){return b[a]}).replace(/,/g,"،")},ordinalParse:/\d{1,2}م/,ordinal:"%dم",week:{dow:6,doy:12}})}),function(a){a(vb)}(function(a){function b(a,b,d,e){var f="";switch(d){case"s":return e?"muutaman sekunnin":"muutama sekunti";case"m":return e?"minuutin":"minuutti";case"mm":f=e?"minuutin":"minuuttia";break;case"h":return e?"tunnin":"tunti";case"hh":f=e?"tunnin":"tuntia";break;case"d":return e?"päivän":"päivä";case"dd":f=e?"päivän":"päivää";break;case"M":return e?"kuukauden":"kuukausi";case"MM":f=e?"kuukauden":"kuukautta";break;case"y":return e?"vuoden":"vuosi";case"yy":f=e?"vuoden":"vuotta"}return f=c(a,e)+" "+f}function c(a,b){return 10>a?b?e[a]:d[a]:a}var d="nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän".split(" "),e=["nolla","yhden","kahden","kolmen","neljän","viiden","kuuden",d[7],d[8],d[9]];return a.defineLocale("fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] LT",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] LT",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] LT",llll:"ddd, Do MMM YYYY, [klo] LT"},calendar:{sameDay:"[tänään] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s päästä",past:"%s sitten",s:b,m:b,mm:b,h:b,hh:b,d:b,dd:b,M:b,MM:b,y:b,yy:b},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("fo",{months:"januar_februar_mars_apríl_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"sunnudagur_mánadagur_týsdagur_mikudagur_hósdagur_fríggjadagur_leygardagur".split("_"),weekdaysShort:"sun_mán_týs_mik_hós_frí_ley".split("_"),weekdaysMin:"su_má_tý_mi_hó_fr_le".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D. MMMM, YYYY LT"},calendar:{sameDay:"[Í dag kl.] LT",nextDay:"[Í morgin kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[Í gjár kl.] LT",lastWeek:"[síðstu] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"um %s",past:"%s síðani",s:"fá sekund",m:"ein minutt",mm:"%d minuttir",h:"ein tími",hh:"%d tímar",d:"ein dagur",dd:"%d dagar",M:"ein mánaði",MM:"%d mánaðir",y:"eitt ár",yy:"%d ár"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("fr-ca",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|)/,ordinal:function(a){return a+(1===a?"er":"")}})}),function(a){a(vb)}(function(a){return a.defineLocale("fr",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|)/,ordinal:function(a){return a+(1===a?"er":"")},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){var b="jan._feb._mrt._apr._mai_jun._jul._aug._sep._okt._nov._des.".split("_"),c="jan_feb_mrt_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_");return a.defineLocale("fy",{months:"jannewaris_febrewaris_maart_april_maaie_juny_july_augustus_septimber_oktober_novimber_desimber".split("_"),monthsShort:function(a,d){return/-MMM-/.test(d)?c[a.month()]:b[a.month()]},weekdays:"snein_moandei_tiisdei_woansdei_tongersdei_freed_sneon".split("_"),weekdaysShort:"si._mo._ti._wo._to._fr._so.".split("_"),weekdaysMin:"Si_Mo_Ti_Wo_To_Fr_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[hjoed om] LT",nextDay:"[moarn om] LT",nextWeek:"dddd [om] LT",lastDay:"[juster om] LT",lastWeek:"[ôfrûne] dddd [om] LT",sameElse:"L"},relativeTime:{future:"oer %s",past:"%s lyn",s:"in pear sekonden",m:"ien minút",mm:"%d minuten",h:"ien oere",hh:"%d oeren",d:"ien dei",dd:"%d dagen",M:"ien moanne",MM:"%d moannen",y:"ien jier",yy:"%d jierren"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(a){return a+(1===a||8===a||a>=20?"ste":"de")},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("gl",{months:"Xaneiro_Febreiro_Marzo_Abril_Maio_Xuño_Xullo_Agosto_Setembro_Outubro_Novembro_Decembro".split("_"),monthsShort:"Xan._Feb._Mar._Abr._Mai._Xuñ._Xul._Ago._Set._Out._Nov._Dec.".split("_"),weekdays:"Domingo_Luns_Martes_Mércores_Xoves_Venres_Sábado".split("_"),weekdaysShort:"Dom._Lun._Mar._Mér._Xov._Ven._Sáb.".split("_"),weekdaysMin:"Do_Lu_Ma_Mé_Xo_Ve_Sá".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:function(){return"[hoxe "+(1!==this.hours()?"ás":"á")+"] LT"},nextDay:function(){return"[mañá "+(1!==this.hours()?"ás":"á")+"] LT"},nextWeek:function(){return"dddd ["+(1!==this.hours()?"ás":"a")+"] LT"},lastDay:function(){return"[onte "+(1!==this.hours()?"á":"a")+"] LT"},lastWeek:function(){return"[o] dddd [pasado "+(1!==this.hours()?"ás":"a")+"] LT"},sameElse:"L"},relativeTime:{future:function(a){return"uns segundos"===a?"nuns segundos":"en "+a},past:"hai %s",s:"uns segundos",m:"un minuto",mm:"%d minutos",h:"unha hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){return a.defineLocale("he",{months:"ינואר_פברואר_מרץ_אפריל_מאי_יוני_יולי_אוגוסט_ספטמבר_אוקטובר_נובמבר_דצמבר".split("_"),monthsShort:"ינו׳_פבר׳_מרץ_אפר׳_מאי_יוני_יולי_אוג׳_ספט׳_אוק׳_נוב׳_דצמ׳".split("_"),weekdays:"ראשון_שני_שלישי_רביעי_חמישי_שישי_שבת".split("_"),weekdaysShort:"א׳_ב׳_ג׳_ד׳_ה׳_ו׳_ש׳".split("_"),weekdaysMin:"א_ב_ג_ד_ה_ו_ש".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [ב]MMMM YYYY",LLL:"D [ב]MMMM YYYY LT",LLLL:"dddd, D [ב]MMMM YYYY LT",l:"D/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY LT",llll:"ddd, D MMM YYYY LT"},calendar:{sameDay:"[היום ב־]LT",nextDay:"[מחר ב־]LT",nextWeek:"dddd [בשעה] LT",lastDay:"[אתמול ב־]LT",lastWeek:"[ביום] dddd [האחרון בשעה] LT",sameElse:"L"},relativeTime:{future:"בעוד %s",past:"לפני %s",s:"מספר שניות",m:"דקה",mm:"%d דקות",h:"שעה",hh:function(a){return 2===a?"שעתיים":a+" שעות"},d:"יום",dd:function(a){return 2===a?"יומיים":a+" ימים"},M:"חודש",MM:function(a){return 2===a?"חודשיים":a+" חודשים"},y:"שנה",yy:function(a){return 2===a?"שנתיים":a%10===0&&10!==a?a+" שנה":a+" שנים"}}})}),function(a){a(vb)}(function(a){var b={1:"१",2:"२",3:"३",4:"४",5:"५",6:"६",7:"७",8:"८",9:"९",0:"०"},c={"१":"1","२":"2","३":"3","४":"4","५":"5","६":"6","७":"7","८":"8","९":"9","०":"0"};return a.defineLocale("hi",{months:"जनवरी_फ़रवरी_मार्च_अप्रैल_मई_जून_जुलाई_अगस्त_सितम्बर_अक्टूबर_नवम्बर_दिसम्बर".split("_"),monthsShort:"जन._फ़र._मार्च_अप्रै._मई_जून_जुल._अग._सित._अक्टू._नव._दिस.".split("_"),weekdays:"रविवार_सोमवार_मंगलवार_बुधवार_गुरूवार_शुक्रवार_शनिवार".split("_"),weekdaysShort:"रवि_सोम_मंगल_बुध_गुरू_शुक्र_शनि".split("_"),weekdaysMin:"र_सो_मं_बु_गु_शु_श".split("_"),longDateFormat:{LT:"A h:mm बजे",LTS:"A h:mm:ss बजे",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, LT",LLLL:"dddd, D MMMM YYYY, LT"},calendar:{sameDay:"[आज] LT",nextDay:"[कल] LT",nextWeek:"dddd, LT",lastDay:"[कल] LT",lastWeek:"[पिछले] dddd, LT",sameElse:"L"},relativeTime:{future:"%s में",past:"%s पहले",s:"कुछ ही क्षण",m:"एक मिनट",mm:"%d मिनट",h:"एक घंटा",hh:"%d घंटे",d:"एक दिन",dd:"%d दिन",M:"एक महीने",MM:"%d महीने",y:"एक वर्ष",yy:"%d वर्ष"},preparse:function(a){return a.replace(/[१२३४५६७८९०]/g,function(a){return c[a]})},postformat:function(a){return a.replace(/\d/g,function(a){return b[a]})},meridiemParse:/रात|सुबह|दोपहर|शाम/,meridiemHour:function(a,b){return 12===a&&(a=0),"रात"===b?4>a?a:a+12:"सुबह"===b?a:"दोपहर"===b?a>=10?a:a+12:"शाम"===b?a+12:void 0},meridiem:function(a){return 4>a?"रात":10>a?"सुबह":17>a?"दोपहर":20>a?"शाम":"रात"},week:{dow:0,doy:6}})}),function(a){a(vb)}(function(a){function b(a,b,c){var d=a+" ";switch(c){case"m":return b?"jedna minuta":"jedne minute";case"mm":return d+=1===a?"minuta":2===a||3===a||4===a?"minute":"minuta";case"h":return b?"jedan sat":"jednog sata";case"hh":return d+=1===a?"sat":2===a||3===a||4===a?"sata":"sati";case"dd":return d+=1===a?"dan":"dana";case"MM":return d+=1===a?"mjesec":2===a||3===a||4===a?"mjeseca":"mjeseci";case"yy":return d+=1===a?"godina":2===a||3===a||4===a?"godine":"godina"}}return a.defineLocale("hr",{months:"sječanj_veljača_ožujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac".split("_"),monthsShort:"sje._vel._ožu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.".split("_"),weekdays:"nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._čet._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_če_pe_su".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[jučer u] LT",lastWeek:function(){switch(this.day()){case 0:case 3:return"[prošlu] dddd [u] LT";case 6:return"[prošle] [subote] [u] LT";case 1:case 2:case 4:case 5:return"[prošli] dddd [u] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"par sekundi",m:b,mm:b,h:b,hh:b,d:"dan",dd:b,M:"mjesec",MM:b,y:"godinu",yy:b},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){function b(a,b,c,d){var e=a;switch(c){case"s":return d||b?"néhány másodperc":"néhány másodperce";case"m":return"egy"+(d||b?" perc":" perce");case"mm":return e+(d||b?" perc":" perce");case"h":return"egy"+(d||b?" óra":" órája");case"hh":return e+(d||b?" óra":" órája");case"d":return"egy"+(d||b?" nap":" napja");case"dd":return e+(d||b?" nap":" napja");case"M":return"egy"+(d||b?" hónap":" hónapja");case"MM":return e+(d||b?" hónap":" hónapja");case"y":return"egy"+(d||b?" év":" éve");case"yy":return e+(d||b?" év":" éve")}return""}function c(a){return(a?"":"[múlt] ")+"["+d[this.day()]+"] LT[-kor]"}var d="vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton".split(" ");return a.defineLocale("hu",{months:"január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"),monthsShort:"jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"),weekdays:"vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"),weekdaysShort:"vas_hét_kedd_sze_csüt_pén_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D., LT",LLLL:"YYYY. MMMM D., dddd LT"},meridiemParse:/de|du/i,isPM:function(a){return"u"===a.charAt(1).toLowerCase()},meridiem:function(a,b,c){return 12>a?c===!0?"de":"DE":c===!0?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return c.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return c.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s múlva",past:"%s",s:b,m:b,mm:b,h:b,hh:b,d:b,dd:b,M:b,MM:b,y:b,yy:b},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){function b(a,b){var c={nominative:"հունվար_փետրվար_մարտ_ապրիլ_մայիս_հունիս_հուլիս_օգոստոս_սեպտեմբեր_հոկտեմբեր_նոյեմբեր_դեկտեմբեր".split("_"),accusative:"հունվարի_փետրվարի_մարտի_ապրիլի_մայիսի_հունիսի_հուլիսի_օգոստոսի_սեպտեմբերի_հոկտեմբերի_նոյեմբերի_դեկտեմբերի".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function c(a){var b="հնվ_փտր_մրտ_ապր_մյս_հնս_հլս_օգս_սպտ_հկտ_նմբ_դկտ".split("_");return b[a.month()]}function d(a){var b="կիրակի_երկուշաբթի_երեքշաբթի_չորեքշաբթի_հինգշաբթի_ուրբաթ_շաբաթ".split("_");return b[a.day()]}return a.defineLocale("hy-am",{months:b,monthsShort:c,weekdays:d,weekdaysShort:"կրկ_երկ_երք_չրք_հնգ_ուրբ_շբթ".split("_"),weekdaysMin:"կրկ_երկ_երք_չրք_հնգ_ուրբ_շբթ".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY թ.",LLL:"D MMMM YYYY թ., LT",LLLL:"dddd, D MMMM YYYY թ., LT"},calendar:{sameDay:"[այսօր] LT",nextDay:"[վաղը] LT",lastDay:"[երեկ] LT",nextWeek:function(){return"dddd [օրը ժամը] LT"},lastWeek:function(){return"[անցած] dddd [օրը ժամը] LT"},sameElse:"L"},relativeTime:{future:"%s հետո",past:"%s առաջ",s:"մի քանի վայրկյան",m:"րոպե",mm:"%d րոպե",h:"ժամ",hh:"%d ժամ",d:"օր",dd:"%d օր",M:"ամիս",MM:"%d ամիս",y:"տարի",yy:"%d տարի"},meridiemParse:/գիշերվա|առավոտվա|ցերեկվա|երեկոյան/,isPM:function(a){return/^(ցերեկվա|երեկոյան)$/.test(a)},meridiem:function(a){return 4>a?"գիշերվա":12>a?"առավոտվա":17>a?"ցերեկվա":"երեկոյան"},ordinalParse:/\d{1,2}|\d{1,2}-(ին|րդ)/,ordinal:function(a,b){switch(b){case"DDD":case"w":case"W":case"DDDo":return 1===a?a+"-ին":a+"-րդ";default:return a}},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){return a.defineLocale("id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"LT.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] LT",LLLL:"dddd, D MMMM YYYY [pukul] LT"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(a,b){return 12===a&&(a=0),"pagi"===b?a:"siang"===b?a>=11?a:a+12:"sore"===b||"malam"===b?a+12:void 0},meridiem:function(a){return 11>a?"pagi":15>a?"siang":19>a?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){function b(a){return a%100===11?!0:a%10===1?!1:!0}function c(a,c,d,e){var f=a+" ";switch(d){case"s":return c||e?"nokkrar sekúndur":"nokkrum sekúndum";case"m":return c?"mínúta":"mínútu";case"mm":return b(a)?f+(c||e?"mínútur":"mínútum"):c?f+"mínúta":f+"mínútu";case"hh":return b(a)?f+(c||e?"klukkustundir":"klukkustundum"):f+"klukkustund";case"d":return c?"dagur":e?"dag":"degi";case"dd":return b(a)?c?f+"dagar":f+(e?"daga":"dögum"):c?f+"dagur":f+(e?"dag":"degi");case"M":return c?"mánuður":e?"mánuð":"mánuði";case"MM":return b(a)?c?f+"mánuðir":f+(e?"mánuði":"mánuðum"):c?f+"mánuður":f+(e?"mánuð":"mánuði");case"y":return c||e?"ár":"ári";case"yy":return b(a)?f+(c||e?"ár":"árum"):f+(c||e?"ár":"ári")}}return a.defineLocale("is",{months:"janúar_febrúar_mars_apríl_maí_júní_júlí_ágúst_september_október_nóvember_desember".split("_"),monthsShort:"jan_feb_mar_apr_maí_jún_júl_ágú_sep_okt_nóv_des".split("_"),weekdays:"sunnudagur_mánudagur_þriðjudagur_miðvikudagur_fimmtudagur_föstudagur_laugardagur".split("_"),weekdaysShort:"sun_mán_þri_mið_fim_fös_lau".split("_"),weekdaysMin:"Su_Má_Þr_Mi_Fi_Fö_La".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd, D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[í dag kl.] LT",nextDay:"[á morgun kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[í gær kl.] LT",lastWeek:"[síðasta] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"eftir %s",past:"fyrir %s síðan",s:c,m:c,mm:c,h:"klukkustund",hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato".split("_"),weekdaysShort:"Dom_Lun_Mar_Mer_Gio_Ven_Sab".split("_"),weekdaysMin:"D_L_Ma_Me_G_V_S".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){switch(this.day()){case 0:return"[la scorsa] dddd [alle] LT";default:return"[lo scorso] dddd [alle] LT"}},sameElse:"L"},relativeTime:{future:function(a){return(/^[0-9].+$/.test(a)?"tra":"in")+" "+a},past:"%s fa",s:"alcuni secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("ja",{months:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日".split("_"),weekdaysShort:"日_月_火_水_木_金_土".split("_"),weekdaysMin:"日_月_火_水_木_金_土".split("_"),longDateFormat:{LT:"Ah時m分",LTS:"LTs秒",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日LT",LLLL:"YYYY年M月D日LT dddd"},meridiemParse:/午前|午後/i,isPM:function(a){return"午後"===a},meridiem:function(a){return 12>a?"午前":"午後"},calendar:{sameDay:"[今日] LT",nextDay:"[明日] LT",nextWeek:"[来週]dddd LT",lastDay:"[昨日] LT",lastWeek:"[前週]dddd LT",sameElse:"L"},relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1ヶ月",MM:"%dヶ月",y:"1年",yy:"%d年"}})}),function(a){a(vb)}(function(a){function b(a,b){var c={nominative:"იანვარი_თებერვალი_მარტი_აპრილი_მაისი_ივნისი_ივლისი_აგვისტო_სექტემბერი_ოქტომბერი_ნოემბერი_დეკემბერი".split("_"),accusative:"იანვარს_თებერვალს_მარტს_აპრილის_მაისს_ივნისს_ივლისს_აგვისტს_სექტემბერს_ოქტომბერს_ნოემბერს_დეკემბერს".split("_")},d=/D[oD] *MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function c(a,b){var c={nominative:"კვირა_ორშაბათი_სამშაბათი_ოთხშაბათი_ხუთშაბათი_პარასკევი_შაბათი".split("_"),accusative:"კვირას_ორშაბათს_სამშაბათს_ოთხშაბათს_ხუთშაბათს_პარასკევს_შაბათს".split("_")},d=/(წინა|შემდეგ)/.test(b)?"accusative":"nominative";return c[d][a.day()]}return a.defineLocale("ka",{months:b,monthsShort:"იან_თებ_მარ_აპრ_მაი_ივნ_ივლ_აგვ_სექ_ოქტ_ნოე_დეკ".split("_"),weekdays:c,weekdaysShort:"კვი_ორშ_სამ_ოთხ_ხუთ_პარ_შაბ".split("_"),weekdaysMin:"კვ_ორ_სა_ოთ_ხუ_პა_შა".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[დღეს] LT[-ზე]",nextDay:"[ხვალ] LT[-ზე]",lastDay:"[გუშინ] LT[-ზე]",nextWeek:"[შემდეგ] dddd LT[-ზე]",lastWeek:"[წინა] dddd LT-ზე",sameElse:"L"},relativeTime:{future:function(a){return/(წამი|წუთი|საათი|წელი)/.test(a)?a.replace(/ი$/,"ში"):a+"ში"},past:function(a){return/(წამი|წუთი|საათი|დღე|თვე)/.test(a)?a.replace(/(ი|ე)$/,"ის წინ"):/წელი/.test(a)?a.replace(/წელი$/,"წლის წინ"):void 0},s:"რამდენიმე წამი",m:"წუთი",mm:"%d წუთი",h:"საათი",hh:"%d საათი",d:"დღე",dd:"%d დღე",M:"თვე",MM:"%d თვე",y:"წელი",yy:"%d წელი"},ordinalParse:/0|1-ლი|მე-\d{1,2}|\d{1,2}-ე/,ordinal:function(a){return 0===a?a:1===a?a+"-ლი":20>a||100>=a&&a%20===0||a%100===0?"მე-"+a:a+"-ე"},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){return a.defineLocale("km",{months:"មករា_កុម្ភៈ_មិនា_មេសា_ឧសភា_មិថុនា_កក្កដា_សីហា_កញ្ញា_តុលា_វិច្ឆិកា_ធ្នូ".split("_"),monthsShort:"មករា_កុម្ភៈ_មិនា_មេសា_ឧសភា_មិថុនា_កក្កដា_សីហា_កញ្ញា_តុលា_វិច្ឆិកា_ធ្នូ".split("_"),weekdays:"អាទិត្យ_ច័ន្ទ_អង្គារ_ពុធ_ព្រហស្បតិ៍_សុក្រ_សៅរ៍".split("_"),weekdaysShort:"អាទិត្យ_ច័ន្ទ_អង្គារ_ពុធ_ព្រហស្បតិ៍_សុក្រ_សៅរ៍".split("_"),weekdaysMin:"អាទិត្យ_ច័ន្ទ_អង្គារ_ពុធ_ព្រហស្បតិ៍_សុក្រ_សៅរ៍".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[ថ្ងៃនៈ ម៉ោង] LT",nextDay:"[ស្អែក ម៉ោង] LT",nextWeek:"dddd [ម៉ោង] LT",lastDay:"[ម្សិលមិញ ម៉ោង] LT",lastWeek:"dddd [សប្តាហ៍មុន] [ម៉ោង] LT",sameElse:"L"},relativeTime:{future:"%sទៀត",past:"%sមុន",s:"ប៉ុន្មានវិនាទី",m:"មួយនាទី",mm:"%d នាទី",h:"មួយម៉ោង",hh:"%d ម៉ោង",d:"មួយថ្ងៃ",dd:"%d ថ្ងៃ",M:"មួយខែ",MM:"%d ខែ",y:"មួយឆ្នាំ",yy:"%d ឆ្នាំ"},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("ko",{months:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),monthsShort:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),weekdays:"일요일_월요일_화요일_수요일_목요일_금요일_토요일".split("_"),weekdaysShort:"일_월_화_수_목_금_토".split("_"),weekdaysMin:"일_월_화_수_목_금_토".split("_"),longDateFormat:{LT:"A h시 m분",LTS:"A h시 m분 s초",L:"YYYY.MM.DD",LL:"YYYY년 MMMM D일",LLL:"YYYY년 MMMM D일 LT",LLLL:"YYYY년 MMMM D일 dddd LT"},calendar:{sameDay:"오늘 LT",nextDay:"내일 LT",nextWeek:"dddd LT",lastDay:"어제 LT",lastWeek:"지난주 dddd LT",sameElse:"L"},relativeTime:{future:"%s 후",past:"%s 전",s:"몇초",ss:"%d초",m:"일분",mm:"%d분",h:"한시간",hh:"%d시간",d:"하루",dd:"%d일",M:"한달",MM:"%d달",y:"일년",yy:"%d년"},ordinalParse:/\d{1,2}일/,ordinal:"%d일",meridiemParse:/오전|오후/,isPM:function(a){return"오후"===a},meridiem:function(a){return 12>a?"오전":"오후"}})}),function(a){a(vb)}(function(a){function b(a,b,c){var d={m:["eng Minutt","enger Minutt"],h:["eng Stonn","enger Stonn"],d:["een Dag","engem Dag"],M:["ee Mount","engem Mount"],y:["ee Joer","engem Joer"]};return b?d[c][0]:d[c][1]}function c(a){var b=a.substr(0,a.indexOf(" "));return e(b)?"a "+a:"an "+a}function d(a){var b=a.substr(0,a.indexOf(" "));return e(b)?"viru "+a:"virun "+a}function e(a){if(a=parseInt(a,10),isNaN(a))return!1;if(0>a)return!0;if(10>a)return a>=4&&7>=a?!0:!1;if(100>a){var b=a%10,c=a/10;return e(0===b?c:b)}if(1e4>a){for(;a>=10;)a/=10;return e(a)}return a/=1e3,e(a)}return a.defineLocale("lb",{months:"Januar_Februar_Mäerz_Abrëll_Mee_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Abr._Mee_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),weekdays:"Sonndeg_Méindeg_Dënschdeg_Mëttwoch_Donneschdeg_Freideg_Samschdeg".split("_"),weekdaysShort:"So._Mé._Dë._Më._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mé_Dë_Më_Do_Fr_Sa".split("_"),longDateFormat:{LT:"H:mm [Auer]",LTS:"H:mm:ss [Auer]",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[Haut um] LT",sameElse:"L",nextDay:"[Muer um] LT",nextWeek:"dddd [um] LT",lastDay:"[Gëschter um] LT",lastWeek:function(){switch(this.day()){case 2:case 4:return"[Leschten] dddd [um] LT";default:return"[Leschte] dddd [um] LT"}}},relativeTime:{future:c,past:d,s:"e puer Sekonnen",m:b,mm:"%d Minutten",h:b,hh:"%d Stonnen",d:b,dd:"%d Deeg",M:b,MM:"%d Méint",y:b,yy:"%d Joer"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){function b(a,b,c,d){return b?"kelios sekundės":d?"kelių sekundžių":"kelias sekundes"}function c(a,b,c,d){return b?e(c)[0]:d?e(c)[1]:e(c)[2] +}function d(a){return a%10===0||a>10&&20>a}function e(a){return h[a].split("_")}function f(a,b,f,g){var h=a+" ";return 1===a?h+c(a,b,f[0],g):b?h+(d(a)?e(f)[1]:e(f)[0]):g?h+e(f)[1]:h+(d(a)?e(f)[1]:e(f)[2])}function g(a,b){var c=-1===b.indexOf("dddd HH:mm"),d=i[a.day()];return c?d:d.substring(0,d.length-2)+"į"}var h={m:"minutė_minutės_minutę",mm:"minutės_minučių_minutes",h:"valanda_valandos_valandą",hh:"valandos_valandų_valandas",d:"diena_dienos_dieną",dd:"dienos_dienų_dienas",M:"mėnuo_mėnesio_mėnesį",MM:"mėnesiai_mėnesių_mėnesius",y:"metai_metų_metus",yy:"metai_metų_metus"},i="sekmadienis_pirmadienis_antradienis_trečiadienis_ketvirtadienis_penktadienis_šeštadienis".split("_");return a.defineLocale("lt",{months:"sausio_vasario_kovo_balandžio_gegužės_birželio_liepos_rugpjūčio_rugsėjo_spalio_lapkričio_gruodžio".split("_"),monthsShort:"sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd".split("_"),weekdays:g,weekdaysShort:"Sek_Pir_Ant_Tre_Ket_Pen_Šeš".split("_"),weekdaysMin:"S_P_A_T_K_Pn_Š".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"YYYY [m.] MMMM D [d.]",LLL:"YYYY [m.] MMMM D [d.], LT [val.]",LLLL:"YYYY [m.] MMMM D [d.], dddd, LT [val.]",l:"YYYY-MM-DD",ll:"YYYY [m.] MMMM D [d.]",lll:"YYYY [m.] MMMM D [d.], LT [val.]",llll:"YYYY [m.] MMMM D [d.], ddd, LT [val.]"},calendar:{sameDay:"[Šiandien] LT",nextDay:"[Rytoj] LT",nextWeek:"dddd LT",lastDay:"[Vakar] LT",lastWeek:"[Praėjusį] dddd LT",sameElse:"L"},relativeTime:{future:"po %s",past:"prieš %s",s:b,m:c,mm:f,h:c,hh:f,d:c,dd:f,M:c,MM:f,y:c,yy:f},ordinalParse:/\d{1,2}-oji/,ordinal:function(a){return a+"-oji"},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){function b(a,b,c){var d=a.split("_");return c?b%10===1&&11!==b?d[2]:d[3]:b%10===1&&11!==b?d[0]:d[1]}function c(a,c,e){return a+" "+b(d[e],a,c)}var d={mm:"minūti_minūtes_minūte_minūtes",hh:"stundu_stundas_stunda_stundas",dd:"dienu_dienas_diena_dienas",MM:"mēnesi_mēnešus_mēnesis_mēneši",yy:"gadu_gadus_gads_gadi"};return a.defineLocale("lv",{months:"janvāris_februāris_marts_aprīlis_maijs_jūnijs_jūlijs_augusts_septembris_oktobris_novembris_decembris".split("_"),monthsShort:"jan_feb_mar_apr_mai_jūn_jūl_aug_sep_okt_nov_dec".split("_"),weekdays:"svētdiena_pirmdiena_otrdiena_trešdiena_ceturtdiena_piektdiena_sestdiena".split("_"),weekdaysShort:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysMin:"Sv_P_O_T_C_Pk_S".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"YYYY. [gada] D. MMMM",LLL:"YYYY. [gada] D. MMMM, LT",LLLL:"YYYY. [gada] D. MMMM, dddd, LT"},calendar:{sameDay:"[Šodien pulksten] LT",nextDay:"[Rīt pulksten] LT",nextWeek:"dddd [pulksten] LT",lastDay:"[Vakar pulksten] LT",lastWeek:"[Pagājušā] dddd [pulksten] LT",sameElse:"L"},relativeTime:{future:"%s vēlāk",past:"%s agrāk",s:"dažas sekundes",m:"minūti",mm:c,h:"stundu",hh:c,d:"dienu",dd:c,M:"mēnesi",MM:c,y:"gadu",yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("mk",{months:"јануари_февруари_март_април_мај_јуни_јули_август_септември_октомври_ноември_декември".split("_"),monthsShort:"јан_фев_мар_апр_мај_јун_јул_авг_сеп_окт_ное_дек".split("_"),weekdays:"недела_понеделник_вторник_среда_четврток_петок_сабота".split("_"),weekdaysShort:"нед_пон_вто_сре_чет_пет_саб".split("_"),weekdaysMin:"нe_пo_вт_ср_че_пе_сa".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"D.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Денес во] LT",nextDay:"[Утре во] LT",nextWeek:"dddd [во] LT",lastDay:"[Вчера во] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[Во изминатата] dddd [во] LT";case 1:case 2:case 4:case 5:return"[Во изминатиот] dddd [во] LT"}},sameElse:"L"},relativeTime:{future:"после %s",past:"пред %s",s:"неколку секунди",m:"минута",mm:"%d минути",h:"час",hh:"%d часа",d:"ден",dd:"%d дена",M:"месец",MM:"%d месеци",y:"година",yy:"%d години"},ordinalParse:/\d{1,2}-(ев|ен|ти|ви|ри|ми)/,ordinal:function(a){var b=a%10,c=a%100;return 0===a?a+"-ев":0===c?a+"-ен":c>10&&20>c?a+"-ти":1===b?a+"-ви":2===b?a+"-ри":7===b||8===b?a+"-ми":a+"-ти"},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){return a.defineLocale("ml",{months:"ജനുവരി_ഫെബ്രുവരി_മാർച്ച്_ഏപ്രിൽ_മേയ്_ജൂൺ_ജൂലൈ_ഓഗസ്റ്റ്_സെപ്റ്റംബർ_ഒക്ടോബർ_നവംബർ_ഡിസംബർ".split("_"),monthsShort:"ജനു._ഫെബ്രു._മാർ._ഏപ്രി._മേയ്_ജൂൺ_ജൂലൈ._ഓഗ._സെപ്റ്റ._ഒക്ടോ._നവം._ഡിസം.".split("_"),weekdays:"ഞായറാഴ്ച_തിങ്കളാഴ്ച_ചൊവ്വാഴ്ച_ബുധനാഴ്ച_വ്യാഴാഴ്ച_വെള്ളിയാഴ്ച_ശനിയാഴ്ച".split("_"),weekdaysShort:"ഞായർ_തിങ്കൾ_ചൊവ്വ_ബുധൻ_വ്യാഴം_വെള്ളി_ശനി".split("_"),weekdaysMin:"ഞാ_തി_ചൊ_ബു_വ്യാ_വെ_ശ".split("_"),longDateFormat:{LT:"A h:mm -നു",LTS:"A h:mm:ss -നു",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, LT",LLLL:"dddd, D MMMM YYYY, LT"},calendar:{sameDay:"[ഇന്ന്] LT",nextDay:"[നാളെ] LT",nextWeek:"dddd, LT",lastDay:"[ഇന്നലെ] LT",lastWeek:"[കഴിഞ്ഞ] dddd, LT",sameElse:"L"},relativeTime:{future:"%s കഴിഞ്ഞ്",past:"%s മുൻപ്",s:"അൽപ നിമിഷങ്ങൾ",m:"ഒരു മിനിറ്റ്",mm:"%d മിനിറ്റ്",h:"ഒരു മണിക്കൂർ",hh:"%d മണിക്കൂർ",d:"ഒരു ദിവസം",dd:"%d ദിവസം",M:"ഒരു മാസം",MM:"%d മാസം",y:"ഒരു വർഷം",yy:"%d വർഷം"},meridiemParse:/രാത്രി|രാവിലെ|ഉച്ച കഴിഞ്ഞ്|വൈകുന്നേരം|രാത്രി/i,isPM:function(a){return/^(ഉച്ച കഴിഞ്ഞ്|വൈകുന്നേരം|രാത്രി)$/.test(a)},meridiem:function(a){return 4>a?"രാത്രി":12>a?"രാവിലെ":17>a?"ഉച്ച കഴിഞ്ഞ്":20>a?"വൈകുന്നേരം":"രാത്രി"}})}),function(a){a(vb)}(function(a){var b={1:"१",2:"२",3:"३",4:"४",5:"५",6:"६",7:"७",8:"८",9:"९",0:"०"},c={"१":"1","२":"2","३":"3","४":"4","५":"5","६":"6","७":"7","८":"8","९":"9","०":"0"};return a.defineLocale("mr",{months:"जानेवारी_फेब्रुवारी_मार्च_एप्रिल_मे_जून_जुलै_ऑगस्ट_सप्टेंबर_ऑक्टोबर_नोव्हेंबर_डिसेंबर".split("_"),monthsShort:"जाने._फेब्रु._मार्च._एप्रि._मे._जून._जुलै._ऑग._सप्टें._ऑक्टो._नोव्हें._डिसें.".split("_"),weekdays:"रविवार_सोमवार_मंगळवार_बुधवार_गुरूवार_शुक्रवार_शनिवार".split("_"),weekdaysShort:"रवि_सोम_मंगळ_बुध_गुरू_शुक्र_शनि".split("_"),weekdaysMin:"र_सो_मं_बु_गु_शु_श".split("_"),longDateFormat:{LT:"A h:mm वाजता",LTS:"A h:mm:ss वाजता",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, LT",LLLL:"dddd, D MMMM YYYY, LT"},calendar:{sameDay:"[आज] LT",nextDay:"[उद्या] LT",nextWeek:"dddd, LT",lastDay:"[काल] LT",lastWeek:"[मागील] dddd, LT",sameElse:"L"},relativeTime:{future:"%s नंतर",past:"%s पूर्वी",s:"सेकंद",m:"एक मिनिट",mm:"%d मिनिटे",h:"एक तास",hh:"%d तास",d:"एक दिवस",dd:"%d दिवस",M:"एक महिना",MM:"%d महिने",y:"एक वर्ष",yy:"%d वर्षे"},preparse:function(a){return a.replace(/[१२३४५६७८९०]/g,function(a){return c[a]})},postformat:function(a){return a.replace(/\d/g,function(a){return b[a]})},meridiemParse:/रात्री|सकाळी|दुपारी|सायंकाळी/,meridiemHour:function(a,b){return 12===a&&(a=0),"रात्री"===b?4>a?a:a+12:"सकाळी"===b?a:"दुपारी"===b?a>=10?a:a+12:"सायंकाळी"===b?a+12:void 0},meridiem:function(a){return 4>a?"रात्री":10>a?"सकाळी":17>a?"दुपारी":20>a?"सायंकाळी":"रात्री"},week:{dow:0,doy:6}})}),function(a){a(vb)}(function(a){return a.defineLocale("ms-my",{months:"Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember".split("_"),monthsShort:"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis".split("_"),weekdays:"Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu".split("_"),weekdaysShort:"Ahd_Isn_Sel_Rab_Kha_Jum_Sab".split("_"),weekdaysMin:"Ah_Is_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"LT.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] LT",LLLL:"dddd, D MMMM YYYY [pukul] LT"},meridiemParse:/pagi|tengahari|petang|malam/,meridiemHour:function(a,b){return 12===a&&(a=0),"pagi"===b?a:"tengahari"===b?a>=11?a:a+12:"petang"===b||"malam"===b?a+12:void 0},meridiem:function(a){return 11>a?"pagi":15>a?"tengahari":19>a?"petang":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Esok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kelmarin pukul] LT",lastWeek:"dddd [lepas pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lepas",s:"beberapa saat",m:"seminit",mm:"%d minit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){var b={1:"၁",2:"၂",3:"၃",4:"၄",5:"၅",6:"၆",7:"၇",8:"၈",9:"၉",0:"၀"},c={"၁":"1","၂":"2","၃":"3","၄":"4","၅":"5","၆":"6","၇":"7","၈":"8","၉":"9","၀":"0"};return a.defineLocale("my",{months:"ဇန်နဝါရီ_ဖေဖော်ဝါရီ_မတ်_ဧပြီ_မေ_ဇွန်_ဇူလိုင်_သြဂုတ်_စက်တင်ဘာ_အောက်တိုဘာ_နိုဝင်ဘာ_ဒီဇင်ဘာ".split("_"),monthsShort:"ဇန်_ဖေ_မတ်_ပြီ_မေ_ဇွန်_လိုင်_သြ_စက်_အောက်_နို_ဒီ".split("_"),weekdays:"တနင်္ဂနွေ_တနင်္လာ_အင်္ဂါ_ဗုဒ္ဓဟူး_ကြာသပတေး_သောကြာ_စနေ".split("_"),weekdaysShort:"နွေ_လာ_င်္ဂါ_ဟူး_ကြာ_သော_နေ".split("_"),weekdaysMin:"နွေ_လာ_င်္ဂါ_ဟူး_ကြာ_သော_နေ".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[ယနေ.] LT [မှာ]",nextDay:"[မနက်ဖြန်] LT [မှာ]",nextWeek:"dddd LT [မှာ]",lastDay:"[မနေ.က] LT [မှာ]",lastWeek:"[ပြီးခဲ့သော] dddd LT [မှာ]",sameElse:"L"},relativeTime:{future:"လာမည့် %s မှာ",past:"လွန်ခဲ့သော %s က",s:"စက္ကန်.အနည်းငယ်",m:"တစ်မိနစ်",mm:"%d မိနစ်",h:"တစ်နာရီ",hh:"%d နာရီ",d:"တစ်ရက်",dd:"%d ရက်",M:"တစ်လ",MM:"%d လ",y:"တစ်နှစ်",yy:"%d နှစ်"},preparse:function(a){return a.replace(/[၁၂၃၄၅၆၇၈၉၀]/g,function(a){return c[a]})},postformat:function(a){return a.replace(/\d/g,function(a){return b[a]})},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tirs_ons_tors_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"H.mm",LTS:"LT.ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i går kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"for %s siden",s:"noen sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",M:"en måned",MM:"%d måneder",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){var b={1:"१",2:"२",3:"३",4:"४",5:"५",6:"६",7:"७",8:"८",9:"९",0:"०"},c={"१":"1","२":"2","३":"3","४":"4","५":"5","६":"6","७":"7","८":"8","९":"9","०":"0"};return a.defineLocale("ne",{months:"जनवरी_फेब्रुवरी_मार्च_अप्रिल_मई_जुन_जुलाई_अगष्ट_सेप्टेम्बर_अक्टोबर_नोभेम्बर_डिसेम्बर".split("_"),monthsShort:"जन._फेब्रु._मार्च_अप्रि._मई_जुन_जुलाई._अग._सेप्ट._अक्टो._नोभे._डिसे.".split("_"),weekdays:"आइतबार_सोमबार_मङ्गलबार_बुधबार_बिहिबार_शुक्रबार_शनिबार".split("_"),weekdaysShort:"आइत._सोम._मङ्गल._बुध._बिहि._शुक्र._शनि.".split("_"),weekdaysMin:"आइ._सो._मङ्_बु._बि._शु._श.".split("_"),longDateFormat:{LT:"Aको h:mm बजे",LTS:"Aको h:mm:ss बजे",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, LT",LLLL:"dddd, D MMMM YYYY, LT"},preparse:function(a){return a.replace(/[१२३४५६७८९०]/g,function(a){return c[a]})},postformat:function(a){return a.replace(/\d/g,function(a){return b[a]})},meridiemParse:/राती|बिहान|दिउँसो|बेलुका|साँझ|राती/,meridiemHour:function(a,b){return 12===a&&(a=0),"राती"===b?3>a?a:a+12:"बिहान"===b?a:"दिउँसो"===b?a>=10?a:a+12:"बेलुका"===b||"साँझ"===b?a+12:void 0},meridiem:function(a){return 3>a?"राती":10>a?"बिहान":15>a?"दिउँसो":18>a?"बेलुका":20>a?"साँझ":"राती"},calendar:{sameDay:"[आज] LT",nextDay:"[भोली] LT",nextWeek:"[आउँदो] dddd[,] LT",lastDay:"[हिजो] LT",lastWeek:"[गएको] dddd[,] LT",sameElse:"L"},relativeTime:{future:"%sमा",past:"%s अगाडी",s:"केही समय",m:"एक मिनेट",mm:"%d मिनेट",h:"एक घण्टा",hh:"%d घण्टा",d:"एक दिन",dd:"%d दिन",M:"एक महिना",MM:"%d महिना",y:"एक बर्ष",yy:"%d बर्ष"},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){var b="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),c="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_");return a.defineLocale("nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(a,d){return/-MMM-/.test(d)?c[a.month()]:b[a.month()]},weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"Zo_Ma_Di_Wo_Do_Vr_Za".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",m:"één minuut",mm:"%d minuten",h:"één uur",hh:"%d uur",d:"één dag",dd:"%d dagen",M:"één maand",MM:"%d maanden",y:"één jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(a){return a+(1===a||8===a||a>=20?"ste":"de")},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("nn",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"sundag_måndag_tysdag_onsdag_torsdag_fredag_laurdag".split("_"),weekdaysShort:"sun_mån_tys_ons_tor_fre_lau".split("_"),weekdaysMin:"su_må_ty_on_to_fr_lø".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[I dag klokka] LT",nextDay:"[I morgon klokka] LT",nextWeek:"dddd [klokka] LT",lastDay:"[I går klokka] LT",lastWeek:"[Føregåande] dddd [klokka] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"for %s sidan",s:"nokre sekund",m:"eit minutt",mm:"%d minutt",h:"ein time",hh:"%d timar",d:"ein dag",dd:"%d dagar",M:"ein månad",MM:"%d månader",y:"eit år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){function b(a){return 5>a%10&&a%10>1&&~~(a/10)%10!==1}function c(a,c,d){var e=a+" ";switch(d){case"m":return c?"minuta":"minutę";case"mm":return e+(b(a)?"minuty":"minut");case"h":return c?"godzina":"godzinę";case"hh":return e+(b(a)?"godziny":"godzin");case"MM":return e+(b(a)?"miesiące":"miesięcy");case"yy":return e+(b(a)?"lata":"lat")}}var d="styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień".split("_"),e="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_");return a.defineLocale("pl",{months:function(a,b){return/D MMMM/.test(b)?e[a.month()]:d[a.month()]},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"),weekdays:"niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"),weekdaysShort:"nie_pon_wt_śr_czw_pt_sb".split("_"),weekdaysMin:"N_Pn_Wt_Śr_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Dziś o] LT",nextDay:"[Jutro o] LT",nextWeek:"[W] dddd [o] LT",lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zeszłą niedzielę o] LT";case 3:return"[W zeszłą środę o] LT";case 6:return"[W zeszłą sobotę o] LT";default:return"[W zeszły] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",m:c,mm:c,h:c,hh:c,d:"1 dzień",dd:"%d dni",M:"miesiąc",MM:c,y:"rok",yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("pt-br",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [às] LT",LLLL:"dddd, D [de] MMMM [de] YYYY [às] LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atrás",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº"})}),function(a){a(vb)}(function(a){return a.defineLocale("pt",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"há %s",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){function b(a,b,c){var d={mm:"minute",hh:"ore",dd:"zile",MM:"luni",yy:"ani"},e=" ";return(a%100>=20||a>=100&&a%100===0)&&(e=" de "),a+e+d[c]}return a.defineLocale("ro",{months:"ianuarie_februarie_martie_aprilie_mai_iunie_iulie_august_septembrie_octombrie_noiembrie_decembrie".split("_"),monthsShort:"ian._febr._mart._apr._mai_iun._iul._aug._sept._oct._nov._dec.".split("_"),weekdays:"duminică_luni_marți_miercuri_joi_vineri_sâmbătă".split("_"),weekdaysShort:"Dum_Lun_Mar_Mie_Joi_Vin_Sâm".split("_"),weekdaysMin:"Du_Lu_Ma_Mi_Jo_Vi_Sâ".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[azi la] LT",nextDay:"[mâine la] LT",nextWeek:"dddd [la] LT",lastDay:"[ieri la] LT",lastWeek:"[fosta] dddd [la] LT",sameElse:"L"},relativeTime:{future:"peste %s",past:"%s în urmă",s:"câteva secunde",m:"un minut",mm:b,h:"o oră",hh:b,d:"o zi",dd:b,M:"o lună",MM:b,y:"un an",yy:b},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){function b(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function c(a,c,d){var e={mm:c?"минута_минуты_минут":"минуту_минуты_минут",hh:"час_часа_часов",dd:"день_дня_дней",MM:"месяц_месяца_месяцев",yy:"год_года_лет"};return"m"===d?c?"минута":"минуту":a+" "+b(e[d],+a)}function d(a,b){var c={nominative:"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_"),accusative:"января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function e(a,b){var c={nominative:"янв_фев_март_апр_май_июнь_июль_авг_сен_окт_ноя_дек".split("_"),accusative:"янв_фев_мар_апр_мая_июня_июля_авг_сен_окт_ноя_дек".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function f(a,b){var c={nominative:"воскресенье_понедельник_вторник_среда_четверг_пятница_суббота".split("_"),accusative:"воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу".split("_")},d=/\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/.test(b)?"accusative":"nominative";return c[d][a.day()]}return a.defineLocale("ru",{months:d,monthsShort:e,weekdays:f,weekdaysShort:"вс_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"вс_пн_вт_ср_чт_пт_сб".split("_"),monthsParse:[/^янв/i,/^фев/i,/^мар/i,/^апр/i,/^ма[й|я]/i,/^июн/i,/^июл/i,/^авг/i,/^сен/i,/^окт/i,/^ноя/i,/^дек/i],longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY г.",LLL:"D MMMM YYYY г., LT",LLLL:"dddd, D MMMM YYYY г., LT"},calendar:{sameDay:"[Сегодня в] LT",nextDay:"[Завтра в] LT",lastDay:"[Вчера в] LT",nextWeek:function(){return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT"},lastWeek:function(a){if(a.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В прошлое] dddd [в] LT";case 1:case 2:case 4:return"[В прошлый] dddd [в] LT";case 3:case 5:case 6:return"[В прошлую] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"через %s",past:"%s назад",s:"несколько секунд",m:c,mm:c,h:"час",hh:c,d:"день",dd:c,M:"месяц",MM:c,y:"год",yy:c},meridiemParse:/ночи|утра|дня|вечера/i,isPM:function(a){return/^(дня|вечера)$/.test(a)},meridiem:function(a){return 4>a?"ночи":12>a?"утра":17>a?"дня":"вечера"},ordinalParse:/\d{1,2}-(й|го|я)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":return a+"-й";case"D":return a+"-го";case"w":case"W":return a+"-я";default:return a}},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){function b(a){return a>1&&5>a}function c(a,c,d,e){var f=a+" ";switch(d){case"s":return c||e?"pár sekúnd":"pár sekundami";case"m":return c?"minúta":e?"minútu":"minútou";case"mm":return c||e?f+(b(a)?"minúty":"minút"):f+"minútami";break;case"h":return c?"hodina":e?"hodinu":"hodinou";case"hh":return c||e?f+(b(a)?"hodiny":"hodín"):f+"hodinami";break;case"d":return c||e?"deň":"dňom";case"dd":return c||e?f+(b(a)?"dni":"dní"):f+"dňami";break;case"M":return c||e?"mesiac":"mesiacom";case"MM":return c||e?f+(b(a)?"mesiace":"mesiacov"):f+"mesiacmi";break;case"y":return c||e?"rok":"rokom";case"yy":return c||e?f+(b(a)?"roky":"rokov"):f+"rokmi"}}var d="január_február_marec_apríl_máj_jún_júl_august_september_október_november_december".split("_"),e="jan_feb_mar_apr_máj_jún_júl_aug_sep_okt_nov_dec".split("_");return a.defineLocale("sk",{months:d,monthsShort:e,monthsParse:function(a,b){var c,d=[];for(c=0;12>c;c++)d[c]=new RegExp("^"+a[c]+"$|^"+b[c]+"$","i");return d}(d,e),weekdays:"nedeľa_pondelok_utorok_streda_štvrtok_piatok_sobota".split("_"),weekdaysShort:"ne_po_ut_st_št_pi_so".split("_"),weekdaysMin:"ne_po_ut_st_št_pi_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd D. MMMM YYYY LT"},calendar:{sameDay:"[dnes o] LT",nextDay:"[zajtra o] LT",nextWeek:function(){switch(this.day()){case 0:return"[v nedeľu o] LT";case 1:case 2:return"[v] dddd [o] LT";case 3:return"[v stredu o] LT";case 4:return"[vo štvrtok o] LT";case 5:return"[v piatok o] LT";case 6:return"[v sobotu o] LT"}},lastDay:"[včera o] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulú nedeľu o] LT";case 1:case 2:return"[minulý] dddd [o] LT";case 3:return"[minulú stredu o] LT";case 4:case 5:return"[minulý] dddd [o] LT";case 6:return"[minulú sobotu o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"pred %s",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){function b(a,b,c){var d=a+" ";switch(c){case"m":return b?"ena minuta":"eno minuto";case"mm":return d+=1===a?"minuta":2===a?"minuti":3===a||4===a?"minute":"minut";case"h":return b?"ena ura":"eno uro";case"hh":return d+=1===a?"ura":2===a?"uri":3===a||4===a?"ure":"ur";case"dd":return d+=1===a?"dan":"dni";case"MM":return d+=1===a?"mesec":2===a?"meseca":3===a||4===a?"mesece":"mesecev";case"yy":return d+=1===a?"leto":2===a?"leti":3===a||4===a?"leta":"let"}}return a.defineLocale("sl",{months:"januar_februar_marec_april_maj_junij_julij_avgust_september_oktober_november_december".split("_"),monthsShort:"jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.".split("_"),weekdays:"nedelja_ponedeljek_torek_sreda_četrtek_petek_sobota".split("_"),weekdaysShort:"ned._pon._tor._sre._čet._pet._sob.".split("_"),weekdaysMin:"ne_po_to_sr_če_pe_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[danes ob] LT",nextDay:"[jutri ob] LT",nextWeek:function(){switch(this.day()){case 0:return"[v] [nedeljo] [ob] LT";case 3:return"[v] [sredo] [ob] LT";case 6:return"[v] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[v] dddd [ob] LT"}},lastDay:"[včeraj ob] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[prejšnja] dddd [ob] LT";case 1:case 2:case 4:case 5:return"[prejšnji] dddd [ob] LT"}},sameElse:"L"},relativeTime:{future:"čez %s",past:"%s nazaj",s:"nekaj sekund",m:b,mm:b,h:b,hh:b,d:"en dan",dd:b,M:"en mesec",MM:b,y:"eno leto",yy:b},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){return a.defineLocale("sq",{months:"Janar_Shkurt_Mars_Prill_Maj_Qershor_Korrik_Gusht_Shtator_Tetor_Nëntor_Dhjetor".split("_"),monthsShort:"Jan_Shk_Mar_Pri_Maj_Qer_Kor_Gus_Sht_Tet_Nën_Dhj".split("_"),weekdays:"E Diel_E Hënë_E Martë_E Mërkurë_E Enjte_E Premte_E Shtunë".split("_"),weekdaysShort:"Die_Hën_Mar_Mër_Enj_Pre_Sht".split("_"),weekdaysMin:"D_H_Ma_Më_E_P_Sh".split("_"),meridiemParse:/PD|MD/,isPM:function(a){return"M"===a.charAt(0)},meridiem:function(a){return 12>a?"PD":"MD"},longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Sot në] LT",nextDay:"[Nesër në] LT",nextWeek:"dddd [në] LT",lastDay:"[Dje në] LT",lastWeek:"dddd [e kaluar në] LT",sameElse:"L"},relativeTime:{future:"në %s",past:"%s më parë",s:"disa sekonda",m:"një minutë",mm:"%d minuta",h:"një orë",hh:"%d orë",d:"një ditë",dd:"%d ditë",M:"një muaj",MM:"%d muaj",y:"një vit",yy:"%d vite"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){var b={words:{m:["један минут","једне минуте"],mm:["минут","минуте","минута"],h:["један сат","једног сата"],hh:["сат","сата","сати"],dd:["дан","дана","дана"],MM:["месец","месеца","месеци"],yy:["година","године","година"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(a,c,d){var e=b.words[d];return 1===d.length?c?e[0]:e[1]:a+" "+b.correctGrammaticalCase(a,e)}};return a.defineLocale("sr-cyrl",{months:["јануар","фебруар","март","април","мај","јун","јул","август","септембар","октобар","новембар","децембар"],monthsShort:["јан.","феб.","мар.","апр.","мај","јун","јул","авг.","сеп.","окт.","нов.","дец."],weekdays:["недеља","понедељак","уторак","среда","четвртак","петак","субота"],weekdaysShort:["нед.","пон.","уто.","сре.","чет.","пет.","суб."],weekdaysMin:["не","по","ут","ср","че","пе","су"],longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[данас у] LT",nextDay:"[сутра у] LT",nextWeek:function(){switch(this.day()){case 0:return"[у] [недељу] [у] LT";case 3:return"[у] [среду] [у] LT";case 6:return"[у] [суботу] [у] LT";case 1:case 2:case 4:case 5:return"[у] dddd [у] LT"}},lastDay:"[јуче у] LT",lastWeek:function(){var a=["[прошле] [недеље] [у] LT","[прошлог] [понедељка] [у] LT","[прошлог] [уторка] [у] LT","[прошле] [среде] [у] LT","[прошлог] [четвртка] [у] LT","[прошлог] [петка] [у] LT","[прошле] [суботе] [у] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"за %s",past:"пре %s",s:"неколико секунди",m:b.translate,mm:b.translate,h:b.translate,hh:b.translate,d:"дан",dd:b.translate,M:"месец",MM:b.translate,y:"годину",yy:b.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){var b={words:{m:["jedan minut","jedne minute"],mm:["minut","minute","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mesec","meseca","meseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(a,c,d){var e=b.words[d];return 1===d.length?c?e[0]:e[1]:a+" "+b.correctGrammaticalCase(a,e)}};return a.defineLocale("sr",{months:["januar","februar","mart","april","maj","jun","jul","avgust","septembar","oktobar","novembar","decembar"],monthsShort:["jan.","feb.","mar.","apr.","maj","jun","jul","avg.","sep.","okt.","nov.","dec."],weekdays:["nedelja","ponedeljak","utorak","sreda","četvrtak","petak","subota"],weekdaysShort:["ned.","pon.","uto.","sre.","čet.","pet.","sub."],weekdaysMin:["ne","po","ut","sr","če","pe","su"],longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[juče u] LT",lastWeek:function(){var a=["[prošle] [nedelje] [u] LT","[prošlog] [ponedeljka] [u] LT","[prošlog] [utorka] [u] LT","[prošle] [srede] [u] LT","[prošlog] [četvrtka] [u] LT","[prošlog] [petka] [u] LT","[prošle] [subote] [u] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",m:b.translate,mm:b.translate,h:b.translate,hh:b.translate,d:"dan",dd:b.translate,M:"mesec",MM:b.translate,y:"godinu",yy:b.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){return a.defineLocale("sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag".split("_"),weekdaysShort:"sön_mån_tis_ons_tor_fre_lör".split("_"),weekdaysMin:"sö_må_ti_on_to_fr_lö".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Igår] LT",nextWeek:"dddd LT",lastWeek:"[Förra] dddd[en] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"för %s sedan",s:"några sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en månad",MM:"%d månader",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}(e|a)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"e":1===b?"a":2===b?"a":"e";return a+c},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("ta",{months:"ஜனவரி_பிப்ரவரி_மார்ச்_ஏப்ரல்_மே_ஜூன்_ஜூலை_ஆகஸ்ட்_செப்டெம்பர்_அக்டோபர்_நவம்பர்_டிசம்பர்".split("_"),monthsShort:"ஜனவரி_பிப்ரவரி_மார்ச்_ஏப்ரல்_மே_ஜூன்_ஜூலை_ஆகஸ்ட்_செப்டெம்பர்_அக்டோபர்_நவம்பர்_டிசம்பர்".split("_"),weekdays:"ஞாயிற்றுக்கிழமை_திங்கட்கிழமை_செவ்வாய்கிழமை_புதன்கிழமை_வியாழக்கிழமை_வெள்ளிக்கிழமை_சனிக்கிழமை".split("_"),weekdaysShort:"ஞாயிறு_திங்கள்_செவ்வாய்_புதன்_வியாழன்_வெள்ளி_சனி".split("_"),weekdaysMin:"ஞா_தி_செ_பு_வி_வெ_ச".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, LT",LLLL:"dddd, D MMMM YYYY, LT"},calendar:{sameDay:"[இன்று] LT",nextDay:"[நாளை] LT",nextWeek:"dddd, LT",lastDay:"[நேற்று] LT",lastWeek:"[கடந்த வாரம்] dddd, LT",sameElse:"L"},relativeTime:{future:"%s இல்",past:"%s முன்",s:"ஒரு சில விநாடிகள்",m:"ஒரு நிமிடம்",mm:"%d நிமிடங்கள்",h:"ஒரு மணி நேரம்",hh:"%d மணி நேரம்",d:"ஒரு நாள்",dd:"%d நாட்கள்",M:"ஒரு மாதம்",MM:"%d மாதங்கள்",y:"ஒரு வருடம்",yy:"%d ஆண்டுகள்"},ordinalParse:/\d{1,2}வது/,ordinal:function(a){return a+"வது"},meridiemParse:/யாமம்|வைகறை|காலை|நண்பகல்|எற்பாடு|மாலை/,meridiem:function(a){return 2>a?" யாமம்":6>a?" வைகறை":10>a?" காலை":14>a?" நண்பகல்":18>a?" எற்பாடு":22>a?" மாலை":" யாமம்"},meridiemHour:function(a,b){return 12===a&&(a=0),"யாமம்"===b?2>a?a:a+12:"வைகறை"===b||"காலை"===b?a:"நண்பகல்"===b&&a>=10?a:a+12},week:{dow:0,doy:6}})}),function(a){a(vb)}(function(a){return a.defineLocale("th",{months:"มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม".split("_"),monthsShort:"มกรา_กุมภา_มีนา_เมษา_พฤษภา_มิถุนา_กรกฎา_สิงหา_กันยา_ตุลา_พฤศจิกา_ธันวา".split("_"),weekdays:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์".split("_"),weekdaysShort:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์".split("_"),weekdaysMin:"อา._จ._อ._พ._พฤ._ศ._ส.".split("_"),longDateFormat:{LT:"H นาฬิกา m นาที",LTS:"LT s วินาที",L:"YYYY/MM/DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY เวลา LT",LLLL:"วันddddที่ D MMMM YYYY เวลา LT"},meridiemParse:/ก่อนเที่ยง|หลังเที่ยง/,isPM:function(a){return"หลังเที่ยง"===a +},meridiem:function(a){return 12>a?"ก่อนเที่ยง":"หลังเที่ยง"},calendar:{sameDay:"[วันนี้ เวลา] LT",nextDay:"[พรุ่งนี้ เวลา] LT",nextWeek:"dddd[หน้า เวลา] LT",lastDay:"[เมื่อวานนี้ เวลา] LT",lastWeek:"[วัน]dddd[ที่แล้ว เวลา] LT",sameElse:"L"},relativeTime:{future:"อีก %s",past:"%sที่แล้ว",s:"ไม่กี่วินาที",m:"1 นาที",mm:"%d นาที",h:"1 ชั่วโมง",hh:"%d ชั่วโมง",d:"1 วัน",dd:"%d วัน",M:"1 เดือน",MM:"%d เดือน",y:"1 ปี",yy:"%d ปี"}})}),function(a){a(vb)}(function(a){return a.defineLocale("tl-ph",{months:"Enero_Pebrero_Marso_Abril_Mayo_Hunyo_Hulyo_Agosto_Setyembre_Oktubre_Nobyembre_Disyembre".split("_"),monthsShort:"Ene_Peb_Mar_Abr_May_Hun_Hul_Ago_Set_Okt_Nob_Dis".split("_"),weekdays:"Linggo_Lunes_Martes_Miyerkules_Huwebes_Biyernes_Sabado".split("_"),weekdaysShort:"Lin_Lun_Mar_Miy_Huw_Biy_Sab".split("_"),weekdaysMin:"Li_Lu_Ma_Mi_Hu_Bi_Sab".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"MM/D/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY LT",LLLL:"dddd, MMMM DD, YYYY LT"},calendar:{sameDay:"[Ngayon sa] LT",nextDay:"[Bukas sa] LT",nextWeek:"dddd [sa] LT",lastDay:"[Kahapon sa] LT",lastWeek:"dddd [huling linggo] LT",sameElse:"L"},relativeTime:{future:"sa loob ng %s",past:"%s ang nakalipas",s:"ilang segundo",m:"isang minuto",mm:"%d minuto",h:"isang oras",hh:"%d oras",d:"isang araw",dd:"%d araw",M:"isang buwan",MM:"%d buwan",y:"isang taon",yy:"%d taon"},ordinalParse:/\d{1,2}/,ordinal:function(a){return a},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){var b={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'üncü",4:"'üncü",100:"'üncü",6:"'ncı",9:"'uncu",10:"'uncu",30:"'uncu",60:"'ıncı",90:"'ıncı"};return a.defineLocale("tr",{months:"Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"),monthsShort:"Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"),weekdaysMin:"Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[bugün saat] LT",nextDay:"[yarın saat] LT",nextWeek:"[haftaya] dddd [saat] LT",lastDay:"[dün] LT",lastWeek:"[geçen hafta] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s önce",s:"birkaç saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir gün",dd:"%d gün",M:"bir ay",MM:"%d ay",y:"bir yıl",yy:"%d yıl"},ordinalParse:/\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/,ordinal:function(a){if(0===a)return a+"'ıncı";var c=a%10,d=a%100-c,e=a>=100?100:null;return a+(b[c]||b[d]||b[e])},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){return a.defineLocale("tzm-latn",{months:"innayr_brˤayrˤ_marˤsˤ_ibrir_mayyw_ywnyw_ywlywz_ɣwšt_šwtanbir_ktˤwbrˤ_nwwanbir_dwjnbir".split("_"),monthsShort:"innayr_brˤayrˤ_marˤsˤ_ibrir_mayyw_ywnyw_ywlywz_ɣwšt_šwtanbir_ktˤwbrˤ_nwwanbir_dwjnbir".split("_"),weekdays:"asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas".split("_"),weekdaysShort:"asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas".split("_"),weekdaysMin:"asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[asdkh g] LT",nextDay:"[aska g] LT",nextWeek:"dddd [g] LT",lastDay:"[assant g] LT",lastWeek:"dddd [g] LT",sameElse:"L"},relativeTime:{future:"dadkh s yan %s",past:"yan %s",s:"imik",m:"minuḍ",mm:"%d minuḍ",h:"saɛa",hh:"%d tassaɛin",d:"ass",dd:"%d ossan",M:"ayowr",MM:"%d iyyirn",y:"asgas",yy:"%d isgasn"},week:{dow:6,doy:12}})}),function(a){a(vb)}(function(a){return a.defineLocale("tzm",{months:"ⵉⵏⵏⴰⵢⵔ_ⴱⵕⴰⵢⵕ_ⵎⴰⵕⵚ_ⵉⴱⵔⵉⵔ_ⵎⴰⵢⵢⵓ_ⵢⵓⵏⵢⵓ_ⵢⵓⵍⵢⵓⵣ_ⵖⵓⵛⵜ_ⵛⵓⵜⴰⵏⴱⵉⵔ_ⴽⵟⵓⴱⵕ_ⵏⵓⵡⴰⵏⴱⵉⵔ_ⴷⵓⵊⵏⴱⵉⵔ".split("_"),monthsShort:"ⵉⵏⵏⴰⵢⵔ_ⴱⵕⴰⵢⵕ_ⵎⴰⵕⵚ_ⵉⴱⵔⵉⵔ_ⵎⴰⵢⵢⵓ_ⵢⵓⵏⵢⵓ_ⵢⵓⵍⵢⵓⵣ_ⵖⵓⵛⵜ_ⵛⵓⵜⴰⵏⴱⵉⵔ_ⴽⵟⵓⴱⵕ_ⵏⵓⵡⴰⵏⴱⵉⵔ_ⴷⵓⵊⵏⴱⵉⵔ".split("_"),weekdays:"ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ".split("_"),weekdaysShort:"ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ".split("_"),weekdaysMin:"ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[ⴰⵙⴷⵅ ⴴ] LT",nextDay:"[ⴰⵙⴽⴰ ⴴ] LT",nextWeek:"dddd [ⴴ] LT",lastDay:"[ⴰⵚⴰⵏⵜ ⴴ] LT",lastWeek:"dddd [ⴴ] LT",sameElse:"L"},relativeTime:{future:"ⴷⴰⴷⵅ ⵙ ⵢⴰⵏ %s",past:"ⵢⴰⵏ %s",s:"ⵉⵎⵉⴽ",m:"ⵎⵉⵏⵓⴺ",mm:"%d ⵎⵉⵏⵓⴺ",h:"ⵙⴰⵄⴰ",hh:"%d ⵜⴰⵙⵙⴰⵄⵉⵏ",d:"ⴰⵙⵙ",dd:"%d oⵙⵙⴰⵏ",M:"ⴰⵢoⵓⵔ",MM:"%d ⵉⵢⵢⵉⵔⵏ",y:"ⴰⵙⴳⴰⵙ",yy:"%d ⵉⵙⴳⴰⵙⵏ"},week:{dow:6,doy:12}})}),function(a){a(vb)}(function(a){function b(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function c(a,c,d){var e={mm:"хвилина_хвилини_хвилин",hh:"година_години_годин",dd:"день_дні_днів",MM:"місяць_місяці_місяців",yy:"рік_роки_років"};return"m"===d?c?"хвилина":"хвилину":"h"===d?c?"година":"годину":a+" "+b(e[d],+a)}function d(a,b){var c={nominative:"січень_лютий_березень_квітень_травень_червень_липень_серпень_вересень_жовтень_листопад_грудень".split("_"),accusative:"січня_лютого_березня_квітня_травня_червня_липня_серпня_вересня_жовтня_листопада_грудня".split("_")},d=/D[oD]? *MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function e(a,b){var c={nominative:"неділя_понеділок_вівторок_середа_четвер_п’ятниця_субота".split("_"),accusative:"неділю_понеділок_вівторок_середу_четвер_п’ятницю_суботу".split("_"),genitive:"неділі_понеділка_вівторка_середи_четверга_п’ятниці_суботи".split("_")},d=/(\[[ВвУу]\]) ?dddd/.test(b)?"accusative":/\[?(?:минулої|наступної)? ?\] ?dddd/.test(b)?"genitive":"nominative";return c[d][a.day()]}function f(a){return function(){return a+"о"+(11===this.hours()?"б":"")+"] LT"}}return a.defineLocale("uk",{months:d,monthsShort:"січ_лют_бер_квіт_трав_черв_лип_серп_вер_жовт_лист_груд".split("_"),weekdays:e,weekdaysShort:"нд_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"нд_пн_вт_ср_чт_пт_сб".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY р.",LLL:"D MMMM YYYY р., LT",LLLL:"dddd, D MMMM YYYY р., LT"},calendar:{sameDay:f("[Сьогодні "),nextDay:f("[Завтра "),lastDay:f("[Вчора "),nextWeek:f("[У] dddd ["),lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return f("[Минулої] dddd [").call(this);case 1:case 2:case 4:return f("[Минулого] dddd [").call(this)}},sameElse:"L"},relativeTime:{future:"за %s",past:"%s тому",s:"декілька секунд",m:c,mm:c,h:"годину",hh:c,d:"день",dd:c,M:"місяць",MM:c,y:"рік",yy:c},meridiemParse:/ночі|ранку|дня|вечора/,isPM:function(a){return/^(дня|вечора)$/.test(a)},meridiem:function(a){return 4>a?"ночі":12>a?"ранку":17>a?"дня":"вечора"},ordinalParse:/\d{1,2}-(й|го)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":case"w":case"W":return a+"-й";case"D":return a+"-го";default:return a}},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){return a.defineLocale("uz",{months:"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_"),monthsShort:"янв_фев_мар_апр_май_июн_июл_авг_сен_окт_ноя_дек".split("_"),weekdays:"Якшанба_Душанба_Сешанба_Чоршанба_Пайшанба_Жума_Шанба".split("_"),weekdaysShort:"Якш_Душ_Сеш_Чор_Пай_Жум_Шан".split("_"),weekdaysMin:"Як_Ду_Се_Чо_Па_Жу_Ша".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"D MMMM YYYY, dddd LT"},calendar:{sameDay:"[Бугун соат] LT [да]",nextDay:"[Эртага] LT [да]",nextWeek:"dddd [куни соат] LT [да]",lastDay:"[Кеча соат] LT [да]",lastWeek:"[Утган] dddd [куни соат] LT [да]",sameElse:"L"},relativeTime:{future:"Якин %s ичида",past:"Бир неча %s олдин",s:"фурсат",m:"бир дакика",mm:"%d дакика",h:"бир соат",hh:"%d соат",d:"бир кун",dd:"%d кун",M:"бир ой",MM:"%d ой",y:"бир йил",yy:"%d йил"},week:{dow:1,doy:7}})}),function(a){a(vb)}(function(a){return a.defineLocale("vi",{months:"tháng 1_tháng 2_tháng 3_tháng 4_tháng 5_tháng 6_tháng 7_tháng 8_tháng 9_tháng 10_tháng 11_tháng 12".split("_"),monthsShort:"Th01_Th02_Th03_Th04_Th05_Th06_Th07_Th08_Th09_Th10_Th11_Th12".split("_"),weekdays:"chủ nhật_thứ hai_thứ ba_thứ tư_thứ năm_thứ sáu_thứ bảy".split("_"),weekdaysShort:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysMin:"CN_T2_T3_T4_T5_T6_T7".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM [năm] YYYY",LLL:"D MMMM [năm] YYYY LT",LLLL:"dddd, D MMMM [năm] YYYY LT",l:"DD/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY LT",llll:"ddd, D MMM YYYY LT"},calendar:{sameDay:"[Hôm nay lúc] LT",nextDay:"[Ngày mai lúc] LT",nextWeek:"dddd [tuần tới lúc] LT",lastDay:"[Hôm qua lúc] LT",lastWeek:"dddd [tuần rồi lúc] LT",sameElse:"L"},relativeTime:{future:"%s tới",past:"%s trước",s:"vài giây",m:"một phút",mm:"%d phút",h:"một giờ",hh:"%d giờ",d:"một ngày",dd:"%d ngày",M:"một tháng",MM:"%d tháng",y:"một năm",yy:"%d năm"},ordinalParse:/\d{1,2}/,ordinal:function(a){return a},week:{dow:1,doy:4}})}),function(a){a(vb)}(function(a){return a.defineLocale("zh-cn",{months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),longDateFormat:{LT:"Ah点mm",LTS:"Ah点m分s秒",L:"YYYY-MM-DD",LL:"YYYY年MMMD日",LLL:"YYYY年MMMD日LT",LLLL:"YYYY年MMMD日ddddLT",l:"YYYY-MM-DD",ll:"YYYY年MMMD日",lll:"YYYY年MMMD日LT",llll:"YYYY年MMMD日ddddLT"},meridiemParse:/凌晨|早上|上午|中午|下午|晚上/,meridiemHour:function(a,b){return 12===a&&(a=0),"凌晨"===b||"早上"===b||"上午"===b?a:"下午"===b||"晚上"===b?a+12:a>=11?a:a+12},meridiem:function(a,b){var c=100*a+b;return 600>c?"凌晨":900>c?"早上":1130>c?"上午":1230>c?"中午":1800>c?"下午":"晚上"},calendar:{sameDay:function(){return 0===this.minutes()?"[今天]Ah[点整]":"[今天]LT"},nextDay:function(){return 0===this.minutes()?"[明天]Ah[点整]":"[明天]LT"},lastDay:function(){return 0===this.minutes()?"[昨天]Ah[点整]":"[昨天]LT"},nextWeek:function(){var b,c;return b=a().startOf("week"),c=this.unix()-b.unix()>=604800?"[下]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},lastWeek:function(){var b,c;return b=a().startOf("week"),c=this.unix()=11?a:a+12:"下午"===b||"晚上"===b?a+12:void 0},meridiem:function(a,b){var c=100*a+b;return 900>c?"早上":1130>c?"上午":1230>c?"中午":1800>c?"下午":"晚上"},calendar:{sameDay:"[今天]LT",nextDay:"[明天]LT",nextWeek:"[下]ddddLT",lastDay:"[昨天]LT",lastWeek:"[上]ddddLT",sameElse:"L"},ordinalParse:/\d{1,2}(日|月|週)/,ordinal:function(a,b){switch(b){case"d":case"D":case"DDD":return a+"日";case"M":return a+"月";case"w":case"W":return a+"週";default:return a}},relativeTime:{future:"%s內",past:"%s前",s:"幾秒",m:"一分鐘",mm:"%d分鐘",h:"一小時",hh:"%d小時",d:"一天",dd:"%d天",M:"一個月",MM:"%d個月",y:"一年",yy:"%d年"}})}),vb.locale("en"),Lb?module.exports=vb:"function"==typeof define&&define.amd?(define(function(a,b,c){return c.config&&c.config()&&c.config().noGlobal===!0&&(zb.moment=wb),vb}),ub(!0)):ub()}).call(this); \ No newline at end of file From 85eb2bde66fb0c4cfbb6772283dcf9711dee5d12 Mon Sep 17 00:00:00 2001 From: cofiem Date: Thu, 5 Mar 2015 23:00:27 +1000 Subject: [PATCH 02/49] convert time zone name to moment js format --- .../shared/_time_zone_select_custom.html.haml | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/app/views/shared/_time_zone_select_custom.html.haml b/app/views/shared/_time_zone_select_custom.html.haml index be280141..ec45d683 100644 --- a/app/views/shared/_time_zone_select_custom.html.haml +++ b/app/views/shared/_time_zone_select_custom.html.haml @@ -33,8 +33,28 @@ var helpInfo = $('##{help_id}'); if(mappingValue){ appendedInfo.text(mappingValue); - var origTime = moment().tz(enteredValue.replace(' - ', '/')); - helpInfo.text('Currently: '+origTime.format("dddd, MMMM Do YYYY, h:mm:ss a z Z")); + + var momentTimezoneName = enteredValue; + + // to make this match with moment js + // replace dashes with slashes + var momentTimezoneName = momentTimezoneName.replace(' - ', '/'); + var momentTimezoneName = momentTimezoneName.replace("'", ''); + + // move place name after comma + var commaIndex = momentTimezoneName.indexOf(', '); + if (commaIndex >= 0){ + var toMove = momentTimezoneName.substring(commaIndex + 2); + momentTimezoneName = momentTimezoneName.replace('/', '/' + toMove + '/'); + momentTimezoneName = momentTimezoneName.substring(0,momentTimezoneName.indexOf(', ')); + } + + // replace spaces with underscores + var momentTimezoneName = momentTimezoneName.replace(' ', '_'); + + var origTime = moment().tz(momentTimezoneName); + var formatted = origTime.format("dddd, MMMM Do YYYY, h:mm:ss a z Z") + helpInfo.text('Currently: ' + formatted); } else { appendedInfo.text('(no match)'); helpInfo.text('(no match)'); From bf49f0627b2c9627351f497d35147b2b277adafb Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 6 Mar 2015 10:27:30 +1000 Subject: [PATCH 03/49] fixed case sensitive bug when changing audio_event tags #166 --- app/models/audio_event.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/audio_event.rb b/app/models/audio_event.rb index 57e3550a..1b99076a 100644 --- a/app/models/audio_event.rb +++ b/app/models/audio_event.rb @@ -312,13 +312,15 @@ def set_tags self.taggings.each do |tagging| tag = tagging.tag - existing_tag = Tag.where(text: tag.text).first + # ensure string comparison is case insensitive + existing_tag = Tag.where('lower(text) = ?', tag.text.downcase).first unless existing_tag.blank? #remove the tag association, otherwise it tries to create the tag and fails (as the tag already exists) self.tags.each do |audio_event_tag| # The collection.delete method removes one or more objects from the collection by setting their foreign keys to NULL. - self.tags.delete(audio_event_tag) if existing_tag.text == audio_event_tag.text + # ensure string comparison is case insensitive + self.tags.delete(audio_event_tag) if existing_tag.text.downcase == audio_event_tag.text.downcase end # remove the tagging association From d437736ec3a2fc015579b8b1080d9446c8586c6c Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 6 Mar 2015 10:53:23 +1000 Subject: [PATCH 04/49] use new way of updating model --- app/controllers/audio_events_controller.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/audio_events_controller.rb b/app/controllers/audio_events_controller.rb index fe3eb99b..2dcfc606 100644 --- a/app/controllers/audio_events_controller.rb +++ b/app/controllers/audio_events_controller.rb @@ -89,8 +89,7 @@ def create # PUT /audio_events/1 # PUT /audio_events/1.json def update - @audio_event.attributes = audio_event_params - if @audio_event.save + if @audio_event.update(audio_event_params) render json: @audio_event.to_json(include: :taggings), status: :created else render json: @audio_event.errors, status: :unprocessable_entity From 617e2786b924cf3737413890b6be2ca3fcb0bbcc Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 6 Mar 2015 10:53:58 +1000 Subject: [PATCH 05/49] updated baw-* gems, removed web-console --- Gemfile | 6 +++--- Gemfile.lock | 33 ++++++++++++--------------------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/Gemfile b/Gemfile index 15cf554f..59ca83c8 100644 --- a/Gemfile +++ b/Gemfile @@ -121,7 +121,7 @@ gem 'raddocs', '~> 0.4.0' # MEDIA # ------------------------------------- # set to a specific commit when releasing to master branch -gem 'baw-audio-tools', git: 'https://github.com/QutBioacoustics/baw-audio-tools.git', branch: :master, ref: '3750e2fc9a' +gem 'baw-audio-tools', git: 'https://github.com/QutBioacoustics/baw-audio-tools.git', branch: :master, ref: '3ea03c6b65' gem 'rack-rewrite', '~> 1.5.1' # ASYNC JOBS @@ -130,7 +130,7 @@ gem 'resque', '~> 1.25.2' gem 'resque-job-stats', git: 'https://github.com/echannel/resque-job-stats.git', branch: :master, ref: '8932c036ae' gem 'resque-status', '~> 0.4.3' # set to a specific commit when releasing to master branch -gem 'baw-workers', git: 'https://github.com/QutBioacoustics/baw-workers.git', branch: :master, ref: 'cac9715c73' +gem 'baw-workers', git: 'https://github.com/QutBioacoustics/baw-workers.git', branch: :master, ref: '9b51d7a82a' # Gems restricted by environment and/or platform # ==================================================== @@ -155,7 +155,7 @@ group :development, :test do gem 'spring', '~> 1.3.0' # Run `rails console` in the browser. Read more: https://github.com/rails/web-console - gem 'web-console', '~> 2.0.0' + #gem 'web-console', '~> 2.1.1' # bundle exec rake doc:rails generates the API under doc/api. gem 'sdoc', '~> 0.4.0' diff --git a/Gemfile.lock b/Gemfile.lock index d7cd84f0..a24cc8bc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,8 +17,8 @@ GIT GIT remote: https://github.com/QutBioacoustics/baw-audio-tools.git - revision: 3750e2fc9a25016da694a752347ead6841ec135a - ref: 3750e2fc9a + revision: 3ea03c6b650b3cde41911fd3214bf6657771b4df + ref: 3ea03c6b65 branch: master specs: baw-audio-tools (0.3.0) @@ -26,8 +26,8 @@ GIT GIT remote: https://github.com/QutBioacoustics/baw-workers.git - revision: cac9715c7371e52928496f037239155ed8e8bed7 - ref: cac9715c73 + revision: 9b51d7a82ac3c995cec5385b91997a0a6b242ef7 + ref: 9b51d7a82a branch: master specs: baw-workers (0.5.0) @@ -110,8 +110,6 @@ GEM addressable (2.3.7) arel (6.0.0) bcrypt (3.1.10) - binding_of_caller (0.7.2) - debug_inspector (>= 0.0.1) bootstrap-datepicker-rails (1.3.1.1) railties (>= 3.0) bootstrap-timepicker-rails (0.1.3) @@ -130,7 +128,7 @@ GEM capistrano-bundler (1.1.4) capistrano (~> 3.1) sshkit (~> 1.2) - capistrano-newrelic (0.0.8) + capistrano-newrelic (0.0.9) capistrano (~> 3.0) newrelic_rpm capistrano-passenger (0.0.2) @@ -152,7 +150,7 @@ GEM timers (~> 4.0.0) climate_control (0.0.3) activesupport (>= 3.0) - cocaine (0.5.5) + cocaine (0.5.7) climate_control (>= 0.0.3, < 1.0) codeclimate-test-reporter (0.4.7) simplecov (>= 0.7.1, < 1.0.0) @@ -175,7 +173,6 @@ GEM safe_yaml (~> 1.0.0) daemons (1.1.9) database_cleaner (1.3.0) - debug_inspector (0.0.2) devise (3.4.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -185,14 +182,14 @@ GEM warden (~> 1.2.3) diff-lcs (1.2.5) docile (1.1.5) - enumerize (0.10.0) + enumerize (0.10.1) activesupport (>= 3.2) erubis (2.7.0) eventmachine (1.0.7) exception_notification (4.0.1) actionmailer (>= 3.0.4) activesupport (>= 3.0.4) - execjs (2.3.0) + execjs (2.4.0) factory_girl (4.5.0) activesupport (>= 3.0.0) factory_girl_rails (4.5.0) @@ -238,7 +235,7 @@ GEM nokogiri (~> 1.6.0) ruby_parser (~> 3.5) i18n (0.7.0) - jbuilder (2.2.8) + jbuilder (2.2.9) activesupport (>= 3.0.0, < 5) multi_json (~> 1.2) jc-validates_timeliness (3.1.1) @@ -268,7 +265,7 @@ GEM mini_portile (0.6.2) minitest (5.5.1) mono_logger (1.1.0) - multi_json (1.10.1) + multi_json (1.11.0) mustache (0.99.8) nenv (0.2.0) net-scp (1.2.1) @@ -426,7 +423,7 @@ GEM actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) - sshkit (1.6.1) + sshkit (1.7.1) colorize (>= 0.7.0) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) @@ -445,7 +442,7 @@ GEM timeliness (0.3.7) timers (4.0.1) hitimes - tins (1.3.4) + tins (1.3.5) tzinfo (1.2.2) thread_safe (~> 0.1) uglifier (2.7.1) @@ -457,11 +454,6 @@ GEM rack (>= 1.0.0) warden (1.2.3) rack (>= 1.0) - web-console (2.0.0) - activemodel (~> 4.0) - binding_of_caller (>= 0.7.2) - railties (~> 4.0) - sprockets-rails (>= 2.0, < 4.0) webmock (1.20.4) addressable (>= 2.3.6) crack (>= 0.3.2) @@ -544,6 +536,5 @@ DEPENDENCIES twitter-bootstrap-rails! uglifier (>= 1.3.0) uuidtools (~> 2.1.5) - web-console (~> 2.0.0) webmock (~> 1.20.4) will_paginate (~> 3.0.7) From 945737e2220514cfb93e88235ec3b910bb9f2ddc Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 6 Mar 2015 10:56:08 +1000 Subject: [PATCH 06/49] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5233711a..f74aaf07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Release 0.13.1](https://github.com/QutBioacoustics/baw-server/releases/tag/0.13.1) (2015-03-02) + - 2015-03-06 + - Fix: ignored ffmpeg warning for channel layout + - Fix: case insensitive compare when chaning audio event tags + - 2015-02-28 - Fix: site paging - Fix: Admin changing user's email From 75c77f847d4db6ac33549f8017750cee3333a077 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sat, 7 Mar 2015 09:50:28 +1000 Subject: [PATCH 07/49] migration and form changes to save user and site time zones --- app/controllers/application_controller.rb | 2 +- app/controllers/sites_controller.rb | 2 +- app/models/site.rb | 15 +- app/models/user.rb | 14 +- app/views/user_accounts/_form.html.haml | 2 +- .../20150306224910_add_timezone_columns.rb | 11 + db/schema.rb | 203 +++++++++--------- lib/modules/time_zone_helper.rb | 18 ++ 8 files changed, 158 insertions(+), 109 deletions(-) create mode 100644 db/migrate/20150306224910_add_timezone_columns.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 548175ae..6671cb1c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -268,7 +268,7 @@ def get_redirect def configure_permitted_parameters devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:user_name, :email, :password, :password_confirmation) } - devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:user_name, :email, :password, :password_confirmation, :current_password, :image) } + devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:user_name, :email, :password, :password_confirmation, :current_password, :image, :tzinfo_tz) } end private diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb index 3d63cc4d..a976091a 100644 --- a/app/controllers/sites_controller.rb +++ b/app/controllers/sites_controller.rb @@ -224,7 +224,7 @@ def get_user_sites end def site_params - params.require(:site).permit(:name, :latitude, :longitude, :description, :image, :notes) + params.require(:site).permit(:name, :latitude, :longitude, :description, :image, :notes, :tzinfo_tz) end def site_show_params diff --git a/app/models/site.rb b/app/models/site.rb index b040e6e0..fb935fbe 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -46,6 +46,8 @@ class Site < ActiveRecord::Base validates_attachment_content_type :image, content_type: /^image\/(jpg|jpeg|pjpeg|png|x-png|gif)$/, message: 'file type %{value} is not allowed (only jpeg/png/gif images)' + before_save :set_rails_tz, if: Proc.new { |site| site.tzinfo_tz_changed? } + # commonly used queries #scope :specified_sites, lambda { |site_ids| where('id in (:ids)', { :ids => site_ids } ) } #scope :sites_in_project, lambda { |project_ids| where(Project.specified_projects, { :ids => project_ids } ) } @@ -119,10 +121,6 @@ def self.add_location_jitter(value, min, max) modified_value end - def tzinfo_tz - 'Australia - Brisbane' - end - # Define filter api settings def self.filter_settings { @@ -137,4 +135,13 @@ def self.filter_settings } } end + + def set_rails_tz + tzInfo_id = TimeZoneHelper.to_identifier(self.tzinfo_tz) + rails_tz_string = TimeZoneHelper.tzinfo_to_ruby(tzInfo_id) + unless rails_tz_string.blank? + self.rails_tz = rails_tz_string + end + end + end \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index 92cb55f6..cdbf4cf6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -117,6 +117,8 @@ def login after_create :special_after_create_actions + before_save :set_rails_tz, if: Proc.new { |user| user.tzinfo_tz_changed? } + def projects # TODO tidy up user project accessing - too many ways to do the same thing (self.created_projects.includes(:sites, :creator) + self.accessible_projects.includes(:sites, :creator)).uniq.sort { |a, b| a.name.downcase <=> b.name.downcase } @@ -336,10 +338,6 @@ def get_membership_duration Time.zone.now - self.created_at end - def tzinfo_tz - 'Australia - Brisbane' - end - def ensure_authentication_token if authentication_token.blank? self.authentication_token = generate_authentication_token @@ -411,4 +409,12 @@ def generate_authentication_token end end + def set_rails_tz + tzInfo_id = TimeZoneHelper.to_identifier(self.tzinfo_tz) + rails_tz_string = TimeZoneHelper.tzinfo_to_ruby(tzInfo_id) + unless rails_tz_string.blank? + self.rails_tz = rails_tz_string + end + end + end \ No newline at end of file diff --git a/app/views/user_accounts/_form.html.haml b/app/views/user_accounts/_form.html.haml index a4e60e55..dd0a6346 100644 --- a/app/views/user_accounts/_form.html.haml +++ b/app/views/user_accounts/_form.html.haml @@ -28,7 +28,7 @@ - [:confirmed_at,:reset_password_sent_at, :remember_created_at, :sign_in_count, :current_sign_in_at, :last_sign_in_at, :current_sign_in_ip, :last_sign_in_ip, :confirmation_sent_at, :failed_attempts,:locked_at, :created_at, :updated_at, - :roles_mask, :image_updated_at].each do |item| + :roles_mask, :image_updated_at, :tzinfo_tz, :rails_tz].each do |item| = render partial: "user_accounts/static_info", locals: {user_attribute_name: item, user: @user} .control_group = f.label :image diff --git a/db/migrate/20150306224910_add_timezone_columns.rb b/db/migrate/20150306224910_add_timezone_columns.rb new file mode 100644 index 00000000..047fcd83 --- /dev/null +++ b/db/migrate/20150306224910_add_timezone_columns.rb @@ -0,0 +1,11 @@ +class AddTimezoneColumns < ActiveRecord::Migration + def change + add_column :users, :tzinfo_tz, :string, null: true, limit: 255 + add_column :users, :rails_tz, :string, null: true, limit: 255 + + add_column :sites, :tzinfo_tz, :string, null: true, limit: 255 + add_column :sites, :rails_tz, :string, null: true, limit: 255 + + add_column :audio_recordings, :recorded_utc_offset, :string, null: true, limit: 20 + end +end diff --git a/db/schema.rb b/db/schema.rb index d6f0231c..5bce7caf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,24 +11,24 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20141115234848) do +ActiveRecord::Schema.define(version: 20150306224910) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "audio_event_comments", force: :cascade do |t| - t.integer "audio_event_id", null: false - t.text "comment", null: false - t.string "flag" + t.integer "audio_event_id", null: false + t.text "comment", null: false + t.string "flag", limit: 255 t.text "flag_explain" t.integer "flagger_id" t.datetime "flagged_at" - t.integer "creator_id", null: false + t.integer "creator_id", null: false t.integer "updater_id" t.integer "deleter_id" t.datetime "deleted_at" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" end create_table "audio_events", force: :cascade do |t| @@ -58,26 +58,27 @@ add_index "audio_events_tags", ["audio_event_id", "tag_id"], name: "index_audio_events_tags_on_audio_event_id_and_tag_id", unique: true, using: :btree create_table "audio_recordings", force: :cascade do |t| - t.string "uuid", limit: 36, null: false - t.integer "uploader_id", null: false - t.datetime "recorded_date", null: false - t.integer "site_id", null: false - t.decimal "duration_seconds", precision: 10, scale: 4, null: false + t.string "uuid", limit: 36, null: false + t.integer "uploader_id", null: false + t.datetime "recorded_date", null: false + t.integer "site_id", null: false + t.decimal "duration_seconds", precision: 10, scale: 4, null: false t.integer "sample_rate_hertz" t.integer "channels" t.integer "bit_rate_bps" - t.string "media_type", null: false - t.integer "data_length_bytes", limit: 8, null: false - t.string "file_hash", limit: 524, null: false - t.string "status", default: "new" + t.string "media_type", limit: 255, null: false + t.integer "data_length_bytes", limit: 8, null: false + t.string "file_hash", limit: 524, null: false + t.string "status", limit: 255, default: "new" t.text "notes" - t.integer "creator_id", null: false + t.integer "creator_id", null: false t.integer "updater_id" t.integer "deleter_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.datetime "deleted_at" - t.string "original_file_name" + t.string "original_file_name", limit: 255 + t.string "recorded_utc_offset", limit: 20 end add_index "audio_recordings", ["created_at", "updated_at"], name: "audio_recordings_created_updated_at", using: :btree @@ -85,34 +86,34 @@ create_table "bookmarks", force: :cascade do |t| t.integer "audio_recording_id" - t.decimal "offset_seconds", precision: 10, scale: 4 - t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "creator_id", null: false + t.decimal "offset_seconds", precision: 10, scale: 4 + t.string "name", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "creator_id", null: false t.integer "updater_id" t.text "description" - t.string "category" + t.string "category", limit: 255 end create_table "datasets", force: :cascade do |t| - t.string "name", null: false + t.string "name", limit: 255, null: false t.time "start_time" t.time "end_time" t.date "start_date" t.date "end_date" - t.string "filters" + t.string "filters", limit: 255 t.integer "number_of_samples" t.integer "number_of_tags" - t.string "types_of_tags" + t.string "types_of_tags", limit: 255 t.text "description" - t.integer "creator_id", null: false + t.integer "creator_id", null: false t.integer "updater_id" - t.integer "project_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "dataset_result_file_name" - t.string "dataset_result_content_type" + t.integer "project_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "dataset_result_file_name", limit: 255 + t.string "dataset_result_content_type", limit: 255 t.integer "dataset_result_file_size" t.datetime "dataset_result_updated_at" t.text "tag_text_filters" @@ -124,45 +125,47 @@ end create_table "jobs", force: :cascade do |t| - t.string "name", null: false - t.string "annotation_name" + t.string "name", limit: 255, null: false + t.string "annotation_name", limit: 255 t.text "script_settings" - t.integer "dataset_id", null: false - t.integer "script_id", null: false - t.integer "creator_id", null: false + t.integer "dataset_id", null: false + t.integer "script_id", null: false + t.integer "creator_id", null: false t.integer "updater_id" t.integer "deleter_id" t.datetime "deleted_at" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.text "description" end create_table "permissions", force: :cascade do |t| - t.integer "creator_id", null: false - t.string "level", null: false - t.integer "project_id", null: false - t.integer "user_id", null: false + t.integer "creator_id", null: false + t.string "level", limit: 255, null: false + t.integer "project_id", null: false + t.integer "user_id" t.integer "updater_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "logged_in_user", default: false, null: false + t.boolean "anonymous_user", default: false, null: false end create_table "projects", force: :cascade do |t| - t.string "name", null: false + t.string "name", limit: 255, null: false t.text "description" - t.string "urn" + t.string "urn", limit: 255 t.text "notes" - t.integer "creator_id", null: false + t.integer "creator_id", null: false t.integer "updater_id" t.integer "deleter_id" t.datetime "deleted_at" - t.string "image_file_name" - t.string "image_content_type" + t.string "image_file_name", limit: 255 + t.string "image_content_type", limit: 255 t.integer "image_file_size" t.datetime "image_updated_at" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "projects_sites", id: false, force: :cascade do |t| @@ -171,84 +174,88 @@ end create_table "scripts", force: :cascade do |t| - t.string "name", null: false - t.string "description" + t.string "name", limit: 255, null: false + t.string "description", limit: 255 t.text "notes" - t.string "settings_file_file_name" - t.string "settings_file_content_type" + t.string "settings_file_file_name", limit: 255 + t.string "settings_file_content_type", limit: 255 t.integer "settings_file_file_size" t.datetime "settings_file_updated_at" - t.string "data_file_file_name" - t.string "data_file_content_type" + t.string "data_file_file_name", limit: 255 + t.string "data_file_content_type", limit: 255 t.integer "data_file_file_size" t.datetime "data_file_updated_at" - t.string "analysis_identifier", null: false - t.decimal "version", precision: 4, scale: 2, default: 0.1, null: false - t.boolean "verified", default: false + t.string "analysis_identifier", limit: 255, null: false + t.decimal "version", precision: 4, scale: 2, default: 0.1, null: false + t.boolean "verified", default: false t.integer "updated_by_script_id" - t.integer "creator_id", null: false - t.datetime "created_at", null: false + t.integer "creator_id", null: false + t.datetime "created_at", null: false end create_table "sites", force: :cascade do |t| - t.string "name", null: false - t.decimal "longitude", precision: 9, scale: 6 - t.decimal "latitude", precision: 9, scale: 6 + t.string "name", limit: 255, null: false + t.decimal "longitude", precision: 9, scale: 6 + t.decimal "latitude", precision: 9, scale: 6 t.text "notes" - t.integer "creator_id", null: false + t.integer "creator_id", null: false t.integer "updater_id" t.integer "deleter_id" t.datetime "deleted_at" - t.string "image_file_name" - t.string "image_content_type" + t.string "image_file_name", limit: 255 + t.string "image_content_type", limit: 255 t.integer "image_file_size" t.datetime "image_updated_at" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.text "description" + t.string "tzinfo_tz", limit: 255 + t.string "rails_tz", limit: 255 end create_table "tags", force: :cascade do |t| - t.string "text", null: false - t.boolean "is_taxanomic", default: false, null: false - t.string "type_of_tag", default: "general", null: false - t.boolean "retired", default: false, null: false + t.string "text", limit: 255, null: false + t.boolean "is_taxanomic", default: false, null: false + t.string "type_of_tag", limit: 255, default: "general", null: false + t.boolean "retired", default: false, null: false t.text "notes" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "creator_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "creator_id", null: false t.integer "updater_id" end create_table "users", force: :cascade do |t| - t.string "email", null: false - t.string "user_name", null: false - t.string "encrypted_password", null: false - t.string "reset_password_token" + t.string "email", limit: 255, null: false + t.string "user_name", limit: 255, null: false + t.string "encrypted_password", limit: 255, null: false + t.string "reset_password_token", limit: 255 t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" - t.string "current_sign_in_ip" - t.string "last_sign_in_ip" - t.string "confirmation_token" + t.string "current_sign_in_ip", limit: 255 + t.string "last_sign_in_ip", limit: 255 + t.string "confirmation_token", limit: 255 t.datetime "confirmed_at" t.datetime "confirmation_sent_at" - t.string "unconfirmed_email" - t.integer "failed_attempts", default: 0 - t.string "unlock_token" + t.string "unconfirmed_email", limit: 255 + t.integer "failed_attempts", default: 0 + t.string "unlock_token", limit: 255 t.datetime "locked_at" - t.string "authentication_token" - t.string "invitation_token" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "authentication_token", limit: 255 + t.string "invitation_token", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "roles_mask" - t.string "image_file_name" - t.string "image_content_type" + t.string "image_file_name", limit: 255 + t.string "image_content_type", limit: 255 t.integer "image_file_size" t.datetime "image_updated_at" t.text "preferences" + t.string "tzinfo_tz", limit: 255 + t.string "rails_tz", limit: 255 end add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree diff --git a/lib/modules/time_zone_helper.rb b/lib/modules/time_zone_helper.rb index 9f82af3c..1b15a06d 100644 --- a/lib/modules/time_zone_helper.rb +++ b/lib/modules/time_zone_helper.rb @@ -28,6 +28,24 @@ def mapping_zone_to_offset ] end + def to_identifier(friendly_name) + found = TZInfo::Timezone.all.select { |tz| friendly_name == tz.friendly_identifier} + if found.size == 1 + found[0].identifier + else + nil + end + end + + def to_friendly(identifier) + found = TZInfo::Timezone.all.select { |tz| identifier == tz.identifier} + if found.size == 1 + found[0].friendly_identifier + else + nil + end + end + # Get the TZInfo Timezone equivalent to the Ruby TimeZone. # @param [string] ruby_tz_name # @return [TZInfo::Timezone] TZInfo Timezone From 0560c6aef1fe1d51d129f17a557954bcf38b9a3c Mon Sep 17 00:00:00 2001 From: cofiem Date: Sat, 7 Mar 2015 10:02:55 +1000 Subject: [PATCH 08/49] Add last_seen-at column for user activity. Has a window of 10 minutes. Closes #167 --- app/controllers/application_controller.rb | 8 + app/views/user_accounts/_form.html.haml | 2 +- app/views/user_accounts/index.html.haml | 8 +- ...0150306235304_add_last_seen_at_to_users.rb | 5 + db/schema.rb | 204 +++++++++--------- 5 files changed, 125 insertions(+), 102 deletions(-) create mode 100644 db/migrate/20150306235304_add_last_seen_at_to_users.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 548175ae..8ef6eb7d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -59,6 +59,9 @@ class ApplicationController < ActionController::Base # based on https://github.com/theepan/userstamp/tree/bf05d832ee27a717ea9455d685c83ae2cfb80310 around_action :set_then_reset_user_stamper + # update users last activity log every 10 minutes + before_action :set_last_seen_at, if: Proc.new { user_signed_in? && (session[:last_seen_at] == nil || session[:last_seen_at] < 10.minutes.ago) } + protected def add_archived_at_header(model) @@ -475,4 +478,9 @@ def set_then_reset_user_stamper end end + def set_last_seen_at + current_user.update_attribute(:last_seen_at, Time.zone.now) + session[:last_seen_at] = Time.zone.now + end + end diff --git a/app/views/user_accounts/_form.html.haml b/app/views/user_accounts/_form.html.haml index a4e60e55..3ef80c00 100644 --- a/app/views/user_accounts/_form.html.haml +++ b/app/views/user_accounts/_form.html.haml @@ -25,7 +25,7 @@ - else Not Confirmed = f.button :submit, 'Confirm User' - - [:confirmed_at,:reset_password_sent_at, :remember_created_at, :sign_in_count, + - [:confirmed_at,:reset_password_sent_at, :remember_created_at, :sign_in_count, :last_seen_at, :current_sign_in_at, :last_sign_in_at, :current_sign_in_ip, :last_sign_in_ip, :confirmation_sent_at, :failed_attempts,:locked_at, :created_at, :updated_at, :roles_mask, :image_updated_at].each do |item| diff --git a/app/views/user_accounts/index.html.haml b/app/views/user_accounts/index.html.haml index fb8af84f..036e82db 100644 --- a/app/views/user_accounts/index.html.haml +++ b/app/views/user_accounts/index.html.haml @@ -14,11 +14,13 @@ %tbody - @users.each do |user| - - last_login = user.current_sign_in_at.blank? ? user.last_sign_in_at : user.current_sign_in_at + - last_seen = user.last_seen_at unless user.last_seen_at.blank? + - last_seen = user.current_sign_in_at unless user.current_sign_in_at.blank? + - last_seen = user.last_sign_in_at %tr %td= link_to "#{user.user_name} (#{user.role_symbols.join(', ')})", user_account_path(user) - %td= last_login.blank? ? '?' : last_login.iso8601 - %td= last_login.blank? ? '?' : "#{distance_of_time_in_words(Time.zone.now, last_login, nil, {vague: true})} ago" + %td= last_seen.blank? ? '?' : last_seen.iso8601 + %td= last_seen.blank? ? '?' : "#{distance_of_time_in_words(Time.zone.now, last_seen, nil, {vague: true})} ago" %td= user.confirmed? ? 'confirmed' : 'no' %td.nolink = link_to user_account_path(user), class: 'btn btn-mini' do diff --git a/db/migrate/20150306235304_add_last_seen_at_to_users.rb b/db/migrate/20150306235304_add_last_seen_at_to_users.rb new file mode 100644 index 00000000..abb67033 --- /dev/null +++ b/db/migrate/20150306235304_add_last_seen_at_to_users.rb @@ -0,0 +1,5 @@ +class AddLastSeenAtToUsers < ActiveRecord::Migration + def change + add_column :users, :last_seen_at, :datetime, null: true + end +end diff --git a/db/schema.rb b/db/schema.rb index d6f0231c..e01c6459 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,24 +11,24 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20141115234848) do +ActiveRecord::Schema.define(version: 20150306235304) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "audio_event_comments", force: :cascade do |t| - t.integer "audio_event_id", null: false - t.text "comment", null: false - t.string "flag" + t.integer "audio_event_id", null: false + t.text "comment", null: false + t.string "flag", limit: 255 t.text "flag_explain" t.integer "flagger_id" t.datetime "flagged_at" - t.integer "creator_id", null: false + t.integer "creator_id", null: false t.integer "updater_id" t.integer "deleter_id" t.datetime "deleted_at" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" end create_table "audio_events", force: :cascade do |t| @@ -58,26 +58,27 @@ add_index "audio_events_tags", ["audio_event_id", "tag_id"], name: "index_audio_events_tags_on_audio_event_id_and_tag_id", unique: true, using: :btree create_table "audio_recordings", force: :cascade do |t| - t.string "uuid", limit: 36, null: false - t.integer "uploader_id", null: false - t.datetime "recorded_date", null: false - t.integer "site_id", null: false - t.decimal "duration_seconds", precision: 10, scale: 4, null: false + t.string "uuid", limit: 36, null: false + t.integer "uploader_id", null: false + t.datetime "recorded_date", null: false + t.integer "site_id", null: false + t.decimal "duration_seconds", precision: 10, scale: 4, null: false t.integer "sample_rate_hertz" t.integer "channels" t.integer "bit_rate_bps" - t.string "media_type", null: false - t.integer "data_length_bytes", limit: 8, null: false - t.string "file_hash", limit: 524, null: false - t.string "status", default: "new" + t.string "media_type", limit: 255, null: false + t.integer "data_length_bytes", limit: 8, null: false + t.string "file_hash", limit: 524, null: false + t.string "status", limit: 255, default: "new" t.text "notes" - t.integer "creator_id", null: false + t.integer "creator_id", null: false t.integer "updater_id" t.integer "deleter_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.datetime "deleted_at" - t.string "original_file_name" + t.string "original_file_name", limit: 255 + t.string "recorded_utc_offset", limit: 20 end add_index "audio_recordings", ["created_at", "updated_at"], name: "audio_recordings_created_updated_at", using: :btree @@ -85,34 +86,34 @@ create_table "bookmarks", force: :cascade do |t| t.integer "audio_recording_id" - t.decimal "offset_seconds", precision: 10, scale: 4 - t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "creator_id", null: false + t.decimal "offset_seconds", precision: 10, scale: 4 + t.string "name", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "creator_id", null: false t.integer "updater_id" t.text "description" - t.string "category" + t.string "category", limit: 255 end create_table "datasets", force: :cascade do |t| - t.string "name", null: false + t.string "name", limit: 255, null: false t.time "start_time" t.time "end_time" t.date "start_date" t.date "end_date" - t.string "filters" + t.string "filters", limit: 255 t.integer "number_of_samples" t.integer "number_of_tags" - t.string "types_of_tags" + t.string "types_of_tags", limit: 255 t.text "description" - t.integer "creator_id", null: false + t.integer "creator_id", null: false t.integer "updater_id" - t.integer "project_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "dataset_result_file_name" - t.string "dataset_result_content_type" + t.integer "project_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "dataset_result_file_name", limit: 255 + t.string "dataset_result_content_type", limit: 255 t.integer "dataset_result_file_size" t.datetime "dataset_result_updated_at" t.text "tag_text_filters" @@ -124,45 +125,47 @@ end create_table "jobs", force: :cascade do |t| - t.string "name", null: false - t.string "annotation_name" + t.string "name", limit: 255, null: false + t.string "annotation_name", limit: 255 t.text "script_settings" - t.integer "dataset_id", null: false - t.integer "script_id", null: false - t.integer "creator_id", null: false + t.integer "dataset_id", null: false + t.integer "script_id", null: false + t.integer "creator_id", null: false t.integer "updater_id" t.integer "deleter_id" t.datetime "deleted_at" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.text "description" end create_table "permissions", force: :cascade do |t| - t.integer "creator_id", null: false - t.string "level", null: false - t.integer "project_id", null: false - t.integer "user_id", null: false + t.integer "creator_id", null: false + t.string "level", limit: 255, null: false + t.integer "project_id", null: false + t.integer "user_id" t.integer "updater_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "logged_in_user", default: false, null: false + t.boolean "anonymous_user", default: false, null: false end create_table "projects", force: :cascade do |t| - t.string "name", null: false + t.string "name", limit: 255, null: false t.text "description" - t.string "urn" + t.string "urn", limit: 255 t.text "notes" - t.integer "creator_id", null: false + t.integer "creator_id", null: false t.integer "updater_id" t.integer "deleter_id" t.datetime "deleted_at" - t.string "image_file_name" - t.string "image_content_type" + t.string "image_file_name", limit: 255 + t.string "image_content_type", limit: 255 t.integer "image_file_size" t.datetime "image_updated_at" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "projects_sites", id: false, force: :cascade do |t| @@ -171,84 +174,89 @@ end create_table "scripts", force: :cascade do |t| - t.string "name", null: false - t.string "description" + t.string "name", limit: 255, null: false + t.string "description", limit: 255 t.text "notes" - t.string "settings_file_file_name" - t.string "settings_file_content_type" + t.string "settings_file_file_name", limit: 255 + t.string "settings_file_content_type", limit: 255 t.integer "settings_file_file_size" t.datetime "settings_file_updated_at" - t.string "data_file_file_name" - t.string "data_file_content_type" + t.string "data_file_file_name", limit: 255 + t.string "data_file_content_type", limit: 255 t.integer "data_file_file_size" t.datetime "data_file_updated_at" - t.string "analysis_identifier", null: false - t.decimal "version", precision: 4, scale: 2, default: 0.1, null: false - t.boolean "verified", default: false + t.string "analysis_identifier", limit: 255, null: false + t.decimal "version", precision: 4, scale: 2, default: 0.1, null: false + t.boolean "verified", default: false t.integer "updated_by_script_id" - t.integer "creator_id", null: false - t.datetime "created_at", null: false + t.integer "creator_id", null: false + t.datetime "created_at", null: false end create_table "sites", force: :cascade do |t| - t.string "name", null: false - t.decimal "longitude", precision: 9, scale: 6 - t.decimal "latitude", precision: 9, scale: 6 + t.string "name", limit: 255, null: false + t.decimal "longitude", precision: 9, scale: 6 + t.decimal "latitude", precision: 9, scale: 6 t.text "notes" - t.integer "creator_id", null: false + t.integer "creator_id", null: false t.integer "updater_id" t.integer "deleter_id" t.datetime "deleted_at" - t.string "image_file_name" - t.string "image_content_type" + t.string "image_file_name", limit: 255 + t.string "image_content_type", limit: 255 t.integer "image_file_size" t.datetime "image_updated_at" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.text "description" + t.string "tzinfo_tz", limit: 255 + t.string "rails_tz", limit: 255 end create_table "tags", force: :cascade do |t| - t.string "text", null: false - t.boolean "is_taxanomic", default: false, null: false - t.string "type_of_tag", default: "general", null: false - t.boolean "retired", default: false, null: false + t.string "text", limit: 255, null: false + t.boolean "is_taxanomic", default: false, null: false + t.string "type_of_tag", limit: 255, default: "general", null: false + t.boolean "retired", default: false, null: false t.text "notes" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "creator_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "creator_id", null: false t.integer "updater_id" end create_table "users", force: :cascade do |t| - t.string "email", null: false - t.string "user_name", null: false - t.string "encrypted_password", null: false - t.string "reset_password_token" + t.string "email", limit: 255, null: false + t.string "user_name", limit: 255, null: false + t.string "encrypted_password", limit: 255, null: false + t.string "reset_password_token", limit: 255 t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" - t.string "current_sign_in_ip" - t.string "last_sign_in_ip" - t.string "confirmation_token" + t.string "current_sign_in_ip", limit: 255 + t.string "last_sign_in_ip", limit: 255 + t.string "confirmation_token", limit: 255 t.datetime "confirmed_at" t.datetime "confirmation_sent_at" - t.string "unconfirmed_email" - t.integer "failed_attempts", default: 0 - t.string "unlock_token" + t.string "unconfirmed_email", limit: 255 + t.integer "failed_attempts", default: 0 + t.string "unlock_token", limit: 255 t.datetime "locked_at" - t.string "authentication_token" - t.string "invitation_token" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "authentication_token", limit: 255 + t.string "invitation_token", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "roles_mask" - t.string "image_file_name" - t.string "image_content_type" + t.string "image_file_name", limit: 255 + t.string "image_content_type", limit: 255 t.integer "image_file_size" t.datetime "image_updated_at" t.text "preferences" + t.string "tzinfo_tz", limit: 255 + t.string "rails_tz", limit: 255 + t.datetime "last_seen_at" end add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree From 94487604ff9d56e171071f4e3b461a2f68f91bf1 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sat, 7 Mar 2015 10:05:08 +1000 Subject: [PATCH 09/49] updated changelog for user last_seen_at --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f74aaf07..3a33817a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + + - 2015-03-07 + - Added last_seen_at column for users. + ## [Release 0.13.1](https://github.com/QutBioacoustics/baw-server/releases/tag/0.13.1) (2015-03-02) - 2015-03-06 From a653caac8e19715ae6746d9894dadcd571df8c3d Mon Sep 17 00:00:00 2001 From: cofiem Date: Sat, 7 Mar 2015 10:36:19 +1000 Subject: [PATCH 10/49] exposed projection fields for exploring alternate implementations of filter for #160 --- lib/modules/filter/query.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/modules/filter/query.rb b/lib/modules/filter/query.rb index d48eb118..1f5c8c23 100644 --- a/lib/modules/filter/query.rb +++ b/lib/modules/filter/query.rb @@ -10,7 +10,7 @@ class Query include Custom attr_reader :key_prefix, :max_limit, :initial_query, :table, :valid_fields, :text_fields, :filter_settings, - :parameters, :filter, :projection, :qsp_text_filter, :qsp_generic_filters, + :parameters, :filter, :projection, :projection_built, :qsp_text_filter, :qsp_generic_filters, :paging, :sorting # Convert a json POST body to an arel query. @@ -39,6 +39,12 @@ def initialize(parameters, query, model, filter_settings) @projection = @parameters[:projection] @projection = nil if @projection.blank? + if has_projection_params? + @projection_built = build_projections(@projection, @table, @valid_fields) + else + @projection_built = build_projections({include: @render_fields}, @table, @valid_fields) + end + @qsp_text_filter = parse_qsp_text(@parameters) @qsp_generic_filters = parse_qsp(nil, @parameters, @key_prefix, @valid_fields) @paging = parse_paging( From b5bea0ae786a31d3fabdfe88509505330547e031 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sat, 7 Mar 2015 10:52:23 +1000 Subject: [PATCH 11/49] added ability to disable filter paging, enforce max items for #160 --- CHANGELOG.md | 1 + lib/modules/filter/parse.rb | 22 +++++++++++++++++----- lib/modules/filter/query.rb | 9 ++++++++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a33817a..1e027f27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - 2015-03-07 - Added last_seen_at column for users. + - Added ability to disable paging for filters and enforced max item count. ## [Release 0.13.1](https://github.com/QutBioacoustics/baw-server/releases/tag/0.13.1) (2015-03-02) diff --git a/lib/modules/filter/parse.rb b/lib/modules/filter/parse.rb index f7caa6a3..717b4eb2 100644 --- a/lib/modules/filter/parse.rb +++ b/lib/modules/filter/parse.rb @@ -13,22 +13,35 @@ module Parse # @param [Hash] params # @param [Integer] default_page # @param [Integer] default_items + # @param [Integer] max_items # @return [Hash] Paging parameters - def parse_paging(params, default_page, default_items) + def parse_paging(params, default_page, default_items, max_items) page, items, offset, limit = nil # qsp page = params[:page] items = params[:items] + disable_paging = params[:disable_paging] # POST body page = params[:paging][:page] if page.blank? && !params[:paging].blank? items = params[:paging][:items] if items.blank? && !params[:paging].blank? + disable_paging = params[:paging][:disable_paging] if disable_paging.blank? && !params[:paging].blank? - # if page or items is set, set the other to default + # set defaults if no setting was found page = default_page if page.blank? items = default_items if items.blank? + # parse disable paging settings + if disable_paging == 'true' + disable_paging = true + else + disable_paging = false + end + + # ensure items is always less than max_items + items = max_items if items.to_i > max_items + # calculate offset if able offset = (page - 1) * items limit = items @@ -40,9 +53,8 @@ def parse_paging(params, default_page, default_items) page = page.to_i items = items.to_i - # will always return offset, limit, page, items - # either all will be nil, or all will be set - {offset: offset, limit: limit, page: page, items: items} + # will always set all options + {offset: offset, limit: limit, page: page, items: items, disable_paging: disable_paging} end # Parse sort parameters. Will use defaults if not specified. diff --git a/lib/modules/filter/query.rb b/lib/modules/filter/query.rb index 1f5c8c23..2004ada7 100644 --- a/lib/modules/filter/query.rb +++ b/lib/modules/filter/query.rb @@ -23,6 +23,7 @@ def initialize(parameters, query, model, filter_settings) @key_prefix = 'filter_' @default_page = 1 @default_items = 500 + @max_items = 500 @table = relation_table(model) @initial_query = !query.nil? && query.is_a?(ActiveRecord::Relation) ? query : relation_all(model) @valid_fields = filter_settings[:valid_fields].map(&:to_sym) @@ -50,7 +51,8 @@ def initialize(parameters, query, model, filter_settings) @paging = parse_paging( @parameters, @default_page, - @default_items) + @default_items, + @max_items) @sorting = parse_sorting( @parameters, filter_settings[:defaults][:order_by], @@ -216,6 +218,7 @@ def query_sort_custom(query, order_by, direction) # @return [ActiveRecord::Relation] query def query_paging(query) return query unless has_paging_params? + return query if is_paging_disabled? apply_paging(query, @paging[:offset], @paging[:limit]) end @@ -232,6 +235,10 @@ def has_paging_params? !@paging[:page].blank? && !@paging[:items].blank? end + def is_paging_disabled? + @paging[:disable_paging] + end + def has_sort_params? !@sorting[:order_by].blank? && !@sorting[:direction].blank? end From f4ff2c3e21015ec98b85837316b93ae08a3ad0c3 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sat, 7 Mar 2015 19:18:07 +1000 Subject: [PATCH 12/49] Some more tweaks to filter paging #160 --- lib/modules/filter/parse.rb | 4 ++-- lib/modules/filter/query.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/modules/filter/parse.rb b/lib/modules/filter/parse.rb index 717b4eb2..052103e4 100644 --- a/lib/modules/filter/parse.rb +++ b/lib/modules/filter/parse.rb @@ -33,14 +33,14 @@ def parse_paging(params, default_page, default_items, max_items) items = default_items if items.blank? # parse disable paging settings - if disable_paging == 'true' + if disable_paging == 'true' || disable_paging == true disable_paging = true else disable_paging = false end # ensure items is always less than max_items - items = max_items if items.to_i > max_items + fail CustomErrors::UnprocessableEntityError, "Number of items requested #{items} exceeded maximum #{max_items}." if items.to_i > max_items # calculate offset if able offset = (page - 1) * items diff --git a/lib/modules/filter/query.rb b/lib/modules/filter/query.rb index 2004ada7..69da151f 100644 --- a/lib/modules/filter/query.rb +++ b/lib/modules/filter/query.rb @@ -236,7 +236,7 @@ def has_paging_params? end def is_paging_disabled? - @paging[:disable_paging] + @paging[:disable_paging] == 'true' || @paging[:disable_paging] == true end def has_sort_params? From b60fa47c243158ad2f5b073a780bbcb11d697b55 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sat, 7 Mar 2015 19:19:28 +1000 Subject: [PATCH 13/49] Added foreign keys and fixed resulting test failures. Closes #151 --- db/migrate/20150307010121_add_foreign_keys.rb | 110 ++++++++++++++++++ db/schema.rb | 49 +++++++- spec/factories/user_factory.rb | 1 - spec/models/bookmark_spec.rb | 28 +++-- spec/models/dataset_spec.rb | 41 ++++--- 5 files changed, 203 insertions(+), 26 deletions(-) create mode 100644 db/migrate/20150307010121_add_foreign_keys.rb diff --git a/db/migrate/20150307010121_add_foreign_keys.rb b/db/migrate/20150307010121_add_foreign_keys.rb new file mode 100644 index 00000000..37753605 --- /dev/null +++ b/db/migrate/20150307010121_add_foreign_keys.rb @@ -0,0 +1,110 @@ +class AddForeignKeys < ActiveRecord::Migration + def up + add_foreign_key "audio_event_comments", "audio_events", name: "audio_event_comments_audio_event_id_fk" + add_foreign_key "audio_event_comments", "users", column: "creator_id", name: "audio_event_comments_creator_id_fk" + add_foreign_key "audio_event_comments", "users", column: "deleter_id", name: "audio_event_comments_deleter_id_fk" + add_foreign_key "audio_event_comments", "users", column: "flagger_id", name: "audio_event_comments_flagger_id_fk" + add_foreign_key "audio_event_comments", "users", column: "updater_id", name: "audio_event_comments_updater_id_fk" + + add_foreign_key "audio_events", "users", column: "creator_id", name: "audio_events_creator_id_fk" + add_foreign_key "audio_events", "users", column: "deleter_id", name: "audio_events_deleter_id_fk" + add_foreign_key "audio_events", "users", column: "updater_id", name: "audio_events_updater_id_fk" + + add_foreign_key "audio_events_tags", "users", column: "creator_id", name: "audio_events_tags_creator_id_fk" + add_foreign_key "audio_events_tags", "tags", name: "audio_events_tags_tag_id_fk" + add_foreign_key "audio_events_tags", "users", column: "updater_id", name: "audio_events_tags_updater_id_fk" + + add_foreign_key "audio_recordings", "users", column: "creator_id", name: "audio_recordings_creator_id_fk" + add_foreign_key "audio_recordings", "users", column: "deleter_id", name: "audio_recordings_deleter_id_fk" + add_foreign_key "audio_recordings", "sites", name: "audio_recordings_site_id_fk" + add_foreign_key "audio_recordings", "users", column: "updater_id", name: "audio_recordings_updater_id_fk" + add_foreign_key "audio_recordings", "users", column: "uploader_id", name: "audio_recordings_uploader_id_fk" + + add_foreign_key "bookmarks", "audio_recordings", name: "bookmarks_audio_recording_id_fk" + add_foreign_key "bookmarks", "users", column: "creator_id", name: "bookmarks_creator_id_fk" + add_foreign_key "bookmarks", "users", column: "updater_id", name: "bookmarks_updater_id_fk" + + add_foreign_key "datasets", "users", column: "creator_id", name: "datasets_creator_id_fk" + add_foreign_key "datasets", "projects", name: "datasets_project_id_fk" + add_foreign_key "datasets", "users", column: "updater_id", name: "datasets_updater_id_fk" + + add_foreign_key "datasets_sites", "datasets", name: "datasets_sites_dataset_id_fk" + add_foreign_key "datasets_sites", "sites", name: "datasets_sites_site_id_fk" + + add_foreign_key "jobs", "users", column: "creator_id", name: "jobs_creator_id_fk" + add_foreign_key "jobs", "datasets", name: "jobs_dataset_id_fk" + add_foreign_key "jobs", "users", column: "deleter_id", name: "jobs_deleter_id_fk" + add_foreign_key "jobs", "scripts", name: "jobs_script_id_fk" + add_foreign_key "jobs", "users", column: "updater_id", name: "jobs_updater_id_fk" + + add_foreign_key "permissions", "projects", name: "permissions_project_id_fk" + add_foreign_key "permissions", "users", column: "updater_id", name: "permissions_updater_id_fk" + + add_foreign_key "projects", "users", column: "creator_id", name: "projects_creator_id_fk" + add_foreign_key "projects", "users", column: "deleter_id", name: "projects_deleter_id_fk" + add_foreign_key "projects", "users", column: "updater_id", name: "projects_updater_id_fk" + + add_foreign_key "projects_sites", "projects", name: "projects_sites_project_id_fk" + add_foreign_key "projects_sites", "sites", name: "projects_sites_site_id_fk" + + add_foreign_key "scripts", "users", column: "creator_id", name: "scripts_creator_id_fk" + add_foreign_key "scripts", "scripts", column: "updated_by_script_id", name: "scripts_updated_by_script_id_fk" + + add_foreign_key "sites", "users", column: "creator_id", name: "sites_creator_id_fk" + add_foreign_key "sites", "users", column: "deleter_id", name: "sites_deleter_id_fk" + add_foreign_key "sites", "users", column: "updater_id", name: "sites_updater_id_fk" + + add_foreign_key "tags", "users", column: "creator_id", name: "tags_creator_id_fk" + add_foreign_key "tags", "users", column: "updater_id", name: "tags_updater_id_fk" + + # these require changes before they will succeed :/ + + # delete audio events where no audio recording matches + execute "DELETE FROM audio_events +WHERE id IN ( + SELECT ae.id + FROM audio_events ae + LEFT OUTER JOIN audio_recordings ar on ae.audio_recording_id = ar.id + WHERE ar.id is null +)" + + add_foreign_key "audio_events", "audio_recordings", name: "audio_events_audio_recording_id_fk" + + # delete the audio_event_tags where the audio event doesn't exist. + + execute "DELETE FROM audio_events_tags +WHERE audio_event_id IN ( + SELECT aet.audio_event_id + FROM audio_events_tags aet + LEFT OUTER JOIN audio_events ae on aet.audio_event_id = ae.id + WHERE ae.id is null +)" + + add_foreign_key "audio_events_tags", "audio_events", name: "audio_events_tags_audio_event_id_fk" + + # delete the permissions where the user or creator do not exist + execute "DELETE FROM permissions +WHERE user_id IN ( + SELECT p.user_id + FROM permissions p + LEFT OUTER JOIN users u on p.user_id = u.id + WHERE u.id is null +)" + + add_foreign_key "permissions", "users", name: "permissions_user_id_fk" + + execute "DELETE FROM permissions +WHERE creator_id IN ( + SELECT p.creator_id + FROM permissions p + LEFT OUTER JOIN users u on p.creator_id = u.id + WHERE u.id is null +)" + + add_foreign_key "permissions", "users", column: "creator_id", name: "permissions_creator_id_fk" + end + + def down + fail ActiveRecord::IrreversibleMigration + end +end diff --git a/db/schema.rb b/db/schema.rb index e01c6459..631e1b79 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150306235304) do +ActiveRecord::Schema.define(version: 20150307010121) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -264,4 +264,51 @@ add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree add_index "users", ["user_name"], name: "users_user_name_unique", unique: true, using: :btree + add_foreign_key "audio_event_comments", "audio_events", name: "audio_event_comments_audio_event_id_fk" + add_foreign_key "audio_event_comments", "users", column: "creator_id", name: "audio_event_comments_creator_id_fk" + add_foreign_key "audio_event_comments", "users", column: "deleter_id", name: "audio_event_comments_deleter_id_fk" + add_foreign_key "audio_event_comments", "users", column: "flagger_id", name: "audio_event_comments_flagger_id_fk" + add_foreign_key "audio_event_comments", "users", column: "updater_id", name: "audio_event_comments_updater_id_fk" + add_foreign_key "audio_events", "audio_recordings", name: "audio_events_audio_recording_id_fk" + add_foreign_key "audio_events", "users", column: "creator_id", name: "audio_events_creator_id_fk" + add_foreign_key "audio_events", "users", column: "deleter_id", name: "audio_events_deleter_id_fk" + add_foreign_key "audio_events", "users", column: "updater_id", name: "audio_events_updater_id_fk" + add_foreign_key "audio_events_tags", "audio_events", name: "audio_events_tags_audio_event_id_fk" + add_foreign_key "audio_events_tags", "tags", name: "audio_events_tags_tag_id_fk" + add_foreign_key "audio_events_tags", "users", column: "creator_id", name: "audio_events_tags_creator_id_fk" + add_foreign_key "audio_events_tags", "users", column: "updater_id", name: "audio_events_tags_updater_id_fk" + add_foreign_key "audio_recordings", "sites", name: "audio_recordings_site_id_fk" + add_foreign_key "audio_recordings", "users", column: "creator_id", name: "audio_recordings_creator_id_fk" + add_foreign_key "audio_recordings", "users", column: "deleter_id", name: "audio_recordings_deleter_id_fk" + add_foreign_key "audio_recordings", "users", column: "updater_id", name: "audio_recordings_updater_id_fk" + add_foreign_key "audio_recordings", "users", column: "uploader_id", name: "audio_recordings_uploader_id_fk" + add_foreign_key "bookmarks", "audio_recordings", name: "bookmarks_audio_recording_id_fk" + add_foreign_key "bookmarks", "users", column: "creator_id", name: "bookmarks_creator_id_fk" + add_foreign_key "bookmarks", "users", column: "updater_id", name: "bookmarks_updater_id_fk" + add_foreign_key "datasets", "projects", name: "datasets_project_id_fk" + add_foreign_key "datasets", "users", column: "creator_id", name: "datasets_creator_id_fk" + add_foreign_key "datasets", "users", column: "updater_id", name: "datasets_updater_id_fk" + add_foreign_key "datasets_sites", "datasets", name: "datasets_sites_dataset_id_fk" + add_foreign_key "datasets_sites", "sites", name: "datasets_sites_site_id_fk" + add_foreign_key "jobs", "datasets", name: "jobs_dataset_id_fk" + add_foreign_key "jobs", "scripts", name: "jobs_script_id_fk" + add_foreign_key "jobs", "users", column: "creator_id", name: "jobs_creator_id_fk" + add_foreign_key "jobs", "users", column: "deleter_id", name: "jobs_deleter_id_fk" + add_foreign_key "jobs", "users", column: "updater_id", name: "jobs_updater_id_fk" + add_foreign_key "permissions", "projects", name: "permissions_project_id_fk" + add_foreign_key "permissions", "users", column: "creator_id", name: "permissions_creator_id_fk" + add_foreign_key "permissions", "users", column: "updater_id", name: "permissions_updater_id_fk" + add_foreign_key "permissions", "users", name: "permissions_user_id_fk" + add_foreign_key "projects", "users", column: "creator_id", name: "projects_creator_id_fk" + add_foreign_key "projects", "users", column: "deleter_id", name: "projects_deleter_id_fk" + add_foreign_key "projects", "users", column: "updater_id", name: "projects_updater_id_fk" + add_foreign_key "projects_sites", "projects", name: "projects_sites_project_id_fk" + add_foreign_key "projects_sites", "sites", name: "projects_sites_site_id_fk" + add_foreign_key "scripts", "scripts", column: "updated_by_script_id", name: "scripts_updated_by_script_id_fk" + add_foreign_key "scripts", "users", column: "creator_id", name: "scripts_creator_id_fk" + add_foreign_key "sites", "users", column: "creator_id", name: "sites_creator_id_fk" + add_foreign_key "sites", "users", column: "deleter_id", name: "sites_deleter_id_fk" + add_foreign_key "sites", "users", column: "updater_id", name: "sites_updater_id_fk" + add_foreign_key "tags", "users", column: "creator_id", name: "tags_creator_id_fk" + add_foreign_key "tags", "users", column: "updater_id", name: "tags_updater_id_fk" end diff --git a/spec/factories/user_factory.rb b/spec/factories/user_factory.rb index 5a8f7c77..eb43c5b6 100644 --- a/spec/factories/user_factory.rb +++ b/spec/factories/user_factory.rb @@ -5,7 +5,6 @@ sequence(:email) { |n| "user#{n}@example.com" } sequence(:authentication_token) { |n| "some random token #{n}" } sequence(:password) { |n| "password #{n}" } - sequence(:id) { |n| n } roles_mask { 2 } # user role diff --git a/spec/models/bookmark_spec.rb b/spec/models/bookmark_spec.rb index a34d4ee5..74832412 100644 --- a/spec/models/bookmark_spec.rb +++ b/spec/models/bookmark_spec.rb @@ -26,8 +26,9 @@ end it 'should not allow duplicate names for the same user (case-insensitive)' do - create(:bookmark, {creator_id: 3, name: 'I love the smell of napalm in the morning.'}) - ss = build(:bookmark, {creator_id: 3, name: 'I LOVE the smell of napalm in the morning.'}) + user = create(:user) + create(:bookmark, {creator: user, name: 'I love the smell of napalm in the morning.'}) + ss = build(:bookmark, {creator: user, name: 'I LOVE the smell of napalm in the morning.'}) expect(ss).not_to be_valid expect(ss.valid?).to be_falsey expect(ss.errors[:name].size).to eq(1) @@ -39,20 +40,24 @@ end it 'should allow duplicate names for different users (case-insensitive)' do - ss1 = create(:bookmark, {creator_id: 3, name: 'You talkin\' to me?'}) + user1 = create(:user) + user2 = create(:user) + user3 = create(:user) + ss1 = create(:bookmark, {creator: user1, name: 'You talkin\' to me?'}) - ss2 = build(:bookmark, {creator_id: 1, name: 'You TALKIN\' to me?'}) + ss2 = build(:bookmark, {creator: user2, name: 'You TALKIN\' to me?'}) expect(ss2.creator_id).not_to eql(ss1.creator_id), "The same user is present for both cases, invalid test!" expect(ss2).to be_valid - ss3 = build(:bookmark, {creator_id: 2, name: 'You talkin\' to me?'}) + ss3 = build(:bookmark, {creator: user3, name: 'You talkin\' to me?'}) expect(ss3.creator_id).not_to eql(ss1.creator_id), "The same user is present for both cases, invalid test!" expect(ss3).to be_valid end it 'should not allow duplicate names for the same user (case-insensitive)' do - create(:bookmark, {creator_id: 3, name: 'I love the smell of napalm in the morning.'}) - ss = build(:bookmark, {creator_id: 3, name: 'I LOVE the smell of napalm in the morning.'}) + user = create(:user) + create(:bookmark, {creator: user, name: 'I love the smell of napalm in the morning.'}) + ss = build(:bookmark, {creator: user, name: 'I LOVE the smell of napalm in the morning.'}) expect(ss).not_to be_valid expect(ss.valid?).to be_falsey expect(ss.errors[:name].size).to eq(1) @@ -64,13 +69,16 @@ end it 'should allow duplicate names for different users (case-insensitive)' do - ss1 = create(:bookmark, {creator_id: 3, name: 'You talkin\' to me?'}) + user1 = create(:user) + user2 = create(:user) + user3 = create(:user) + ss1 = create(:bookmark, {creator: user1, name: 'You talkin\' to me?'}) - ss2 = build(:bookmark, {creator_id: 1, name: 'You TALKIN\' to me?'}) + ss2 = build(:bookmark, {creator: user2, name: 'You TALKIN\' to me?'}) expect(ss2.creator_id).not_to eql(ss1.creator_id), "The same user is present for both cases, invalid test!" expect(ss2).to be_valid - ss3 = build(:bookmark, {creator_id: 2, name: 'You talkin\' to me?'}) + ss3 = build(:bookmark, {creator: user3, name: 'You talkin\' to me?'}) expect(ss3.creator_id).not_to eql(ss1.creator_id), "The same user is present for both cases, invalid test!" expect(ss3).to be_valid end diff --git a/spec/models/dataset_spec.rb b/spec/models/dataset_spec.rb index c74f06f3..ecba8266 100644 --- a/spec/models/dataset_spec.rb +++ b/spec/models/dataset_spec.rb @@ -10,8 +10,9 @@ it 'should not allow duplicate names for the same user (case-insensitive)' do - create(:dataset, {creator_id: 3, name: 'I love the smell of napalm in the morning.'}) - ss = build(:dataset, {creator_id: 3, name: 'I LOVE the smell of napalm in the morning.'}) + user = create(:user) + create(:dataset, {creator: user, name: 'I love the smell of napalm in the morning.'}) + ss = build(:dataset, {creator: user, name: 'I LOVE the smell of napalm in the morning.'}) expect(ss).not_to be_valid expect(ss.valid?).to be_falsey @@ -24,63 +25,75 @@ end it 'should allow duplicate names for different users (case-insensitive)' do - ss1 = create(:dataset, {creator_id: 3, name: 'You talkin\' to me?'}) + user1 = create(:user) + user2 = create(:user) + user3 = create(:user) + ss1 = create(:dataset, {creator: user1, name: 'You talkin\' to me?'}) - ss2 = build(:dataset, {creator_id: 1, name: 'You TALKIN\' to me?'}) + ss2 = build(:dataset, {creator: user2, name: 'You TALKIN\' to me?'}) expect(ss2.creator_id).not_to eql(ss1.creator_id), "The same user is present for both cases, invalid test!" expect(ss2).to be_valid - ss3 = build(:dataset, {creator_id: 2, name: 'You talkin\' to me?'}) + ss3 = build(:dataset, {creator: user3, name: 'You talkin\' to me?'}) expect(ss3.creator_id).not_to eql(ss1.creator_id), "The same user is present for both cases, invalid test!" expect(ss3).to be_valid end it 'should not be an error to give a start date before the end date' do - d = create(:dataset, {creator_id: 1, start_date: '2013-12-01', end_date: '2013-12-10'}) + user = create(:user) + d = create(:dataset, {creator: user, start_date: '2013-12-01', end_date: '2013-12-10'}) expect(d).to be_valid end it 'should not be an error to give equal start and end dates' do - d = create(:dataset, {creator_id: 1, start_date: '2013-12-01', end_date: '2013-12-01'}) + user = create(:user) + d = create(:dataset, {creator: user, start_date: '2013-12-01', end_date: '2013-12-01'}) expect(d).to be_valid end it 'should be an error to give a start date after the end date' do + user = create(:user) expect { - create(:dataset, {creator_id: 1, start_date: '2013-12-01', end_date: '2013-11-15'}) + create(:dataset, {creator: user, start_date: '2013-12-01', end_date: '2013-11-15'}) }.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Start date must be before end date") end it 'should not be an error to give a start time before the end time' do - d = create(:dataset, {creator_id: 1, start_time: '11:30', end_time: '13:45'}) + user = create(:user) + d = create(:dataset, {creator: user, start_time: '11:30', end_time: '13:45'}) expect(d).to be_valid end it 'should be an error to give equal start and end times' do + user = create(:user) expect { - create(:dataset, {creator_id: 1, start_time: '11:30', end_time: '11:30'}) + create(:dataset, {creator: user, start_time: '11:30', end_time: '11:30'}) }.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Start time must not be equal to end time") end it 'should not be an error to give a start time after the end time' do + user = create(:user) # times can wrap around, so a time range overnight can be specified - d = create(:dataset, {creator_id: 1, start_time: '11:45', end_time: '10:20'}) + d = create(:dataset, {creator: user, start_time: '11:45', end_time: '10:20'}) expect(d).to be_valid end it 'should not be an error when tag_text_filters is an empty array' do - d = create(:dataset, {creator_id: 1, tag_text_filters: []}) + user = create(:user) + d = create(:dataset, {creator: user, tag_text_filters: []}) expect(d).to be_valid end it 'should not be an error when tag_text_filters is an array' do - d = create(:dataset, {creator_id: 1, tag_text_filters: %w(one two)}) + user = create(:user) + d = create(:dataset, {creator: user, tag_text_filters: %w(one two)}) expect(d).to be_valid end it 'should be an error when tag_text_filters is not an array' do + user = create(:user) expect { - create(:dataset, {creator_id: 1, tag_text_filters: {}}) + create(:dataset, {creator: user, tag_text_filters: {}}) }.to raise_error(ActiveRecord::SerializationTypeMismatch, 'Attribute was supposed to be a Array, but was a Hash. -- {}') end From fd5ce8b21573aa34e4482ad55026ead8c6fdaa97 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sat, 7 Mar 2015 19:20:38 +1000 Subject: [PATCH 14/49] preparing for adding unique indexes #138 --- app/models/permission.rb | 2 +- app/models/tagging.rb | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/models/permission.rb b/app/models/permission.rb index b1bbdcd9..cf16d222 100644 --- a/app/models/permission.rb +++ b/app/models/permission.rb @@ -19,7 +19,7 @@ class Permission < ActiveRecord::Base validates :creator, existence: true # attribute validations - validates_uniqueness_of :level, scope: [:user_id, :project_id, :level] + validates_uniqueness_of :level, scope: [:user_id, :project_id] validates_presence_of :level, :user, :creator, :project # Define filter api settings diff --git a/app/models/tagging.rb b/app/models/tagging.rb index 210c5416..95e3c5f4 100644 --- a/app/models/tagging.rb +++ b/app/models/tagging.rb @@ -20,7 +20,6 @@ class Tagging < ActiveRecord::Base validates :creator, existence: true # attribute validations - validates_uniqueness_of :audio_event_id, scope: [:tag_id, :audio_event_id], message: 'audio_event_id %{value} must be unique within tag_id and audio_event_id' - validates_uniqueness_of :tag_id, scope: [:tag_id, :audio_event_id], message: 'tag_id %{value} must be unique within tag_id and audio_event_id' + validates_uniqueness_of :audio_event_id, scope: [:tag_id], message: 'audio_event_id %{value} must be unique within tag_id and audio_event_id' end \ No newline at end of file From 015c02fc107f9b9138570629d117c8d5f6515298 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sat, 7 Mar 2015 19:22:11 +1000 Subject: [PATCH 15/49] updated gems --- Gemfile | 1 + Gemfile.lock | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 59ca83c8..52851592 100644 --- a/Gemfile +++ b/Gemfile @@ -84,6 +84,7 @@ gem 'bcrypt', '~> 3.1.9' # don't change the database gems - causes: # Please install the adapter: `gem install activerecord--adapter` ( is not part of the bundle. Add it to Gemfile.) gem 'pg', '~> 0.18.1' +gem 'immigrant' # MODELS # ------------------------------------- diff --git a/Gemfile.lock b/Gemfile.lock index a24cc8bc..942a4515 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -235,7 +235,9 @@ GEM nokogiri (~> 1.6.0) ruby_parser (~> 3.5) i18n (0.7.0) - jbuilder (2.2.9) + immigrant (0.3.0) + activerecord (>= 3.0) + jbuilder (2.2.11) activesupport (>= 3.0.0, < 5) multi_json (~> 1.2) jc-validates_timeliness (3.1.1) @@ -499,6 +501,7 @@ DEPENDENCIES guard-yard (~> 2.1.4) haml (~> 4.0.6) haml-rails (~> 0.8) + immigrant jbuilder (~> 2.2.3) jc-validates_timeliness (~> 3.1.1) jquery-rails (~> 4.0.3) From 0258258e48f686d503ea730307d338adcd9e00f6 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sat, 7 Mar 2015 19:28:30 +1000 Subject: [PATCH 16/49] updated changelog to indicate foreign keys were added --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e027f27..5f73fa17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - 2015-03-07 - Added last_seen_at column for users. - Added ability to disable paging for filters and enforced max item count. + - Added foreign keys ## [Release 0.13.1](https://github.com/QutBioacoustics/baw-server/releases/tag/0.13.1) (2015-03-02) From 4ac963e74ced027707ef859b3f7dd39a17bdbbc2 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sat, 7 Mar 2015 22:37:24 +1000 Subject: [PATCH 17/49] first go at changing site page #164 --- CHANGELOG.md | 8 ++- app/models/audio_event.rb | 10 +++ app/views/sites/show.html.haml | 85 +++++++++++++++--------- config/initializers/date_time_formats.rb | 1 + 4 files changed, 70 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f73fa17..1df94f0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,11 @@ ## Unreleased - 2015-03-07 - - Added last_seen_at column for users. - - Added ability to disable paging for filters and enforced max item count. - - Added foreign keys + - Added last_seen_at column for users (#167) + - Added ability to disable paging for filters and enforced max item count (#160) + - Added foreign keys (#151) + - Added Timezone setting for sites and users (#116) + - Added links to visualise page (#164) ## [Release 0.13.1](https://github.com/QutBioacoustics/baw-server/releases/tag/0.13.1) (2015-03-02) diff --git a/app/models/audio_event.rb b/app/models/audio_event.rb index 1b99076a..950ad66a 100644 --- a/app/models/audio_event.rb +++ b/app/models/audio_event.rb @@ -284,6 +284,16 @@ def get_library_path "/library/#{self.audio_recording_id}/audio_events/#{self.id}" end + def self.in_site(site) + AudioEvent.find_by_sql(["SELECT ae.* +FROM audio_events ae +INNER JOIN audio_recordings ar ON ae.audio_recording_id = ar.id +INNER JOIN sites s ON ar.site_id = s.id +WHERE s.id = :site_id +ORDER BY ae.updated_at DESC +LIMIT 5", {site_id: site.id}]) + end + private # custom validation methods diff --git a/app/views/sites/show.html.haml b/app/views/sites/show.html.haml index 74581b1f..baf384c9 100644 --- a/app/views/sites/show.html.haml +++ b/app/views/sites/show.html.haml @@ -5,42 +5,65 @@ %h1= @site.name %p= @site.description -- if @site.audio_recordings.blank? - %p No audio recordings from this site. -- else - .row-fluid - .span12 - %h3= 'Audio Recordings' +- duration_sum = @site.audio_recordings.sum(:duration_seconds) +- recorded_min = @site.audio_recordings.minimum(:recorded_date) +- recorded_min = recorded_min.in_time_zone(@site.rails_tz) unless @site.rails_tz.blank? +- recorded_max = @site.audio_recordings.maximum(:recorded_date) +- recorded_max = recorded_max.in_time_zone(@site.rails_tz) unless @site.rails_tz.blank? +- recorded_diff = recorded_max - recorded_min +- last_added = @site.audio_recordings.maximum(:updated_at) - = page_entries_info @site_audio_recordings - = will_paginate @site_audio_recordings +%h3 + Audio Recordings - %table.table.table-striped(data-provides='rowlink') - %thead - %tr - %th Recording Start Date - %th Duration - %th +.row-fluid + .span6 + %p + This site contains recordings from #{recorded_min.to_formatted_s(:readable_full_without_seconds)} + to #{recorded_max.to_formatted_s(:readable_full_without_seconds)}. + %p + This site covers + = distance_of_time recorded_diff + and there are recordings for + = distance_of_time duration_sum + of that time. - %tbody - - @site_audio_recordings.each do |audio_recording| - %tr - %td= link_to audio_recording.recorded_date.to_formatted_s(:readable_full), "/listen/#{audio_recording.id}" - %td= "about #{distance_of_time_in_words(audio_recording.duration_seconds, 0, nil, {except: ['seconds']})}" - %td.nolink - = link_to "/listen/#{audio_recording.id}", target: '_self', class: 'btn btn-mini' do - %i{class: 'fa fa-play'} - Play + %ul.nav.nav-pills.nav-stacked{style: 'margin-bottom:0'} + %li.text-center + %a{href: "/visualize?siteId=#{@site.id}"} + %i.fa.fa-eye + Visualise -.row-fluid - .span12 - - if @site.latitude.blank? || @site.longitude.blank? - %p This site does not have a location set. - - else - = gmaps(markers: {data: @site.to_gmaps4rails}, - map_options: gmaps_default_options) + %h4 Recent Annotations + %ul.nav.nav-pills.nav-stacked{style: 'margin-bottom:0'} + - AudioEvent.in_site(@site).each do |ae| + %li + = link_to "#{ae.updated_at.to_formatted_s(:readable_full_without_seconds)} by #{ae.updater.user_name}", ae.get_listen_path - = yield :scripts + - if @site.latitude.blank? || @site.longitude.blank? + .span6.map-placeholder + %span.map-placeholder-text This site does not have a location set. + - else + .span6= gmaps(markers: {data: @site.to_gmaps4rails}, map_options: {zoom: 7, auto_zoom: true}) + = yield :scripts + -#.span12 + -# %h3 Recent Audio Recordings + -# %table.table.table-striped(data-provides='rowlink') + -# %thead + -# %tr + -# %th Recording Start Date + -# %th Duration + -# %th + -# + -# %tbody + -# - @site.audio_recordings.order(recorded_date: :desc).first(10).each do |audio_recording| + -# %tr + -# %td= link_to audio_recording.recorded_date.to_formatted_s(:readable_full), "/listen/#{audio_recording.id}" + -# %td= "about #{distance_of_time_in_words(audio_recording.duration_seconds, 0, nil, {except: ['seconds']})}" + -# %td.nolink + -# = link_to "/listen/#{audio_recording.id}", target: '_self', class: 'btn btn-mini' do + -# %i{class: 'fa fa-play'} + -# Play - content_for :right_sidebar do diff --git a/config/initializers/date_time_formats.rb b/config/initializers/date_time_formats.rb index d048847f..ac1e6326 100644 --- a/config/initializers/date_time_formats.rb +++ b/config/initializers/date_time_formats.rb @@ -2,6 +2,7 @@ Time::DATE_FORMATS[:very_long_time] = '%H:%M:%S.%3N' Time::DATE_FORMATS[:readable_very_long_time] = '%H hr %M min %S.%3N sec' Time::DATE_FORMATS[:readable_full] = lambda { |time| time.strftime("%a, #{ActiveSupport::Inflector.ordinalize(time.day)} %b %Y at %H:%M:%S #{time.formatted_offset(false)}") } +Time::DATE_FORMATS[:readable_full_without_seconds] = lambda { |time| time.strftime("%a, #{ActiveSupport::Inflector.ordinalize(time.day)} %b %Y at %H:%M (#{time.formatted_offset(false)})") } Date::DATE_FORMATS[:month_and_year] = '%B %Y' Date::DATE_FORMATS[:short_ordinal] = lambda { |date| date.strftime("%B #{date.day.ordinalize}") } Time::DATE_FORMATS[:long_year] = '%Y/%m/%d' \ No newline at end of file From cf1a88bfbde979015a62ef5c9b938c3787245ca2 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sun, 8 Mar 2015 21:54:16 +1000 Subject: [PATCH 18/49] Fixes for site obfuscation, prepared for project permissions changes. Refactored permission code to prepare for project logged in and anon permissions #99 --- CHANGELOG.md | 35 ++- app/controllers/analysis_controller.rb | 2 +- .../audio_event_comments_controller.rb | 6 +- app/controllers/audio_events_controller.rb | 3 +- .../audio_recordings_controller.rb | 19 +- app/controllers/bookmarks_controller.rb | 4 +- app/controllers/media_controller.rb | 2 +- app/controllers/projects_controller.rb | 12 +- app/controllers/public_controller.rb | 28 +- app/controllers/sites_controller.rb | 6 +- app/controllers/user_accounts_controller.rb | 12 +- app/models/ability.rb | 37 +-- app/models/audio_event.rb | 26 +- app/models/dataset.rb | 2 +- app/models/permission.rb | 2 +- app/models/project.rb | 6 +- app/models/site.rb | 54 ++-- app/models/user.rb | 239 ++-------------- app/views/devise/registrations/edit.html.haml | 2 +- app/views/permissions/index.html.haml | 6 +- .../projects/_project_thumb_large.html.haml | 4 +- app/views/projects/show.html.haml | 7 +- app/views/shared/_navbar.html.haml | 4 +- app/views/sites/show.html.haml | 81 +++--- .../user_accounts/_sidebar_links.html.haml | 8 +- app/views/user_accounts/index.html.haml | 4 +- app/views/user_accounts/projects.html.haml | 3 +- app/views/user_accounts/show.html.haml | 12 +- config/routes.rb | 2 +- lib/modules/access/check.rb | 76 +++++ lib/modules/access/core.rb | 267 ++++++++++++++++++ lib/modules/access/query.rb | 205 ++++++++++++++ lib/modules/filter/custom.rb | 41 --- spec/features/sites_spec.rb | 8 +- 34 files changed, 763 insertions(+), 462 deletions(-) create mode 100644 lib/modules/access/check.rb create mode 100644 lib/modules/access/core.rb create mode 100644 lib/modules/access/query.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 1df94f0c..b7456f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,25 +2,32 @@ ## Unreleased + - 2015-03-08 + - Improved site lat/long obfuscation calculation + - Refactored permission code to prepare for project logged in and anon permissions (this was a quite large and sweeping change) [#99](https://github.com/QutBioacoustics/baw-server/issues/99) + - 2015-03-07 - - Added last_seen_at column for users (#167) - - Added ability to disable paging for filters and enforced max item count (#160) - - Added foreign keys (#151) - - Added Timezone setting for sites and users (#116) - - Added links to visualise page (#164) + - Added last_seen_at column for users [#167] + - Added ability to disable paging for filters and enforced max item count [#160] + - Added foreign keys [#151] + - Added Timezone setting for sites and users [#116] + - Added links to visualise page [#155] + - Modify site show page to remove audio recording list [#164] ## [Release 0.13.1](https://github.com/QutBioacoustics/baw-server/releases/tag/0.13.1) (2015-03-02) - 2015-03-06 - - Fix: ignored ffmpeg warning for channel layout - - Fix: case insensitive compare when chaning audio event tags + - Fix: ignored ffmpeg warning for channel layout + - Fix: case insensitive compare when chaning audio event tags + - Added admin-only page to fix orphaned audio recordings [#153] + - 2015-02-28 - - Fix: site paging - - Fix: Admin changing user's email - - Enhancement: additional user info for Admin - - Fix: error accessing sites/filter when logged in as Admin - - Enhancement: added tests to ensure numbers in json are not quoted + - Fix: site paging + - Fix: Admin changing user's email [#158] + - Enhancement: additional user info for Admin [#159] + - Fix: error accessing sites/filter when logged in as Admin + - Enhancement: added tests to ensure numbers in json are not quoted [#152] ## [Release 0.13.0](https://github.com/QutBioacoustics/baw-server/releases/tag/0.13.0) (2015-02-20) @@ -31,7 +38,7 @@ - added rake task to export audio recordings to csv - 2015-01-24 - - Fixed annotation library not filtering using query string parameters. + - Fixed annotation library not filtering using query string parameters [#148] - 2015-01-18 - delete actions improved, archive headers added and fixed @@ -39,7 +46,7 @@ - added audio event filter action - 2015-01-06 - - Fixed CORS responses. + - Fixed CORS responses [#140] - Added ability to poll Resque for job completion rather than polling filesystem. - Bug fix: added more strict validation and more tests for 'in' filter. diff --git a/app/controllers/analysis_controller.rb b/app/controllers/analysis_controller.rb index 5beb9b8e..cbee0ba0 100644 --- a/app/controllers/analysis_controller.rb +++ b/app/controllers/analysis_controller.rb @@ -62,7 +62,7 @@ def show def authorise_custom(request_params, user) # Can't do anything if not logged in, not in user or admin role, or not confirmed - if user.blank? || (!user.has_role?(:user) && !user.has_role?(:admin)) || !user.confirmed? + if user.blank? || (!Access::Check.is_standard_user?(user) && !Access::Check.is_admin?(user)) || !user.confirmed? fail CanCan::AccessDenied, 'Anonymous users, non-admin and non-users, or unconfirmed users cannot access analysis data.' end diff --git a/app/controllers/audio_event_comments_controller.rb b/app/controllers/audio_event_comments_controller.rb index 85ac034f..71161361 100644 --- a/app/controllers/audio_event_comments_controller.rb +++ b/app/controllers/audio_event_comments_controller.rb @@ -15,7 +15,7 @@ def index #@audio_event_comments = AudioEventComment.accessible_by @audio_event_comments, constructed_options = Settings.api_response.response_index( api_filter_params, - current_user.is_admin? ? AudioEventComment.all : current_user.accessible_comments, + Access::Query.audio_event_comments(current_user, Access::Core.levels_allow), AudioEventComment, AudioEventComment.filter_settings ) @@ -55,7 +55,7 @@ def update # allow any logged in user to flag an audio comment # only the user that created the audio comment (or admin) can update any other attribute is_creator = @audio_event_comment.creator.id == current_user.id - is_admin = current_user.has_role?(:admin) + is_admin = Access::Check.is_admin?(current_user) is_changing_only_flag = (audio_event_comment_update_params.include?(:audio_event_comment) && ([:flag] - audio_event_comment_update_params[:audio_event_comment].symbolize_keys.keys).empty?) @@ -84,7 +84,7 @@ def destroy def filter filter_response = Settings.api_response.response_filter( api_filter_params, - current_user.is_admin? ? AudioEventComment.all : current_user.accessible_comments, + Access::Query.audio_event_comments(current_user, Access::Core.levels_allow), AudioEventComment, AudioEventComment.filter_settings ) diff --git a/app/controllers/audio_events_controller.rb b/app/controllers/audio_events_controller.rb index 2dcfc606..6ddff9db 100644 --- a/app/controllers/audio_events_controller.rb +++ b/app/controllers/audio_events_controller.rb @@ -107,8 +107,7 @@ def destroy def filter filter_response = Settings.api_response.response_filter( api_filter_params, - # TODO: allow access to reference audio events as well. - current_user.is_admin? ? AudioEvent.all : current_user.accessible_audio_events, + Access::Query.audio_events(current_user, Access::Core.levels_allow), AudioEvent, AudioEvent.filter_settings ) diff --git a/app/controllers/audio_recordings_controller.rb b/app/controllers/audio_recordings_controller.rb index df8e3828..5f8c7627 100644 --- a/app/controllers/audio_recordings_controller.rb +++ b/app/controllers/audio_recordings_controller.rb @@ -53,9 +53,11 @@ def create uploader_id = audio_recording_params[:uploader_id].to_i user_exists = User.exists?(uploader_id) user = User.where(id: uploader_id).first - highest_permission = user.highest_permission(@project) + actual_level = Access::Query.level_project(user, @project) + requested_level = :writer + is_allowed = Access::Check.allowed?(requested_level, actual_level) - if !user_exists || highest_permission < AccessLevel::WRITE + if !user_exists || !is_allowed render json: {error: 'uploader does not have access to this project'}.to_json, status: :unprocessable_entity elsif check_and_correct_overlap(@audio_recording) && @audio_recording.save render json: @audio_recording, status: :created, location: @audio_recording @@ -107,14 +109,17 @@ def check_uploader if current_user.blank? render json: {error: 'not logged in'}.to_json, status: :unauthorized else - if current_user.has_role? :harvester + if Access::Check.is_harvester?(current_user) # auth check is skipped, so auth is checked manually here uploader_id = params[:uploader_id].to_i user_exists = User.exists?(uploader_id) user = User.where(id: uploader_id).first - highest_permission = user.highest_permission(@project) - if !user_exists || highest_permission < AccessLevel::WRITE + actual_level = Access::Query.level_project(user, @project) + requested_level = :writer + is_allowed = Access::Check.allowed?(requested_level, actual_level) + + if !user_exists || !is_allowed render json: {error: 'uploader does not have access to this project'}.to_json, status: :ok else head :no_content @@ -136,7 +141,7 @@ def update_status def filter filter_response = Settings.api_response.response_filter( api_filter_params, - current_user.is_admin? ? AudioRecording.all : current_user.accessible_audio_recordings, + Access::Query.audio_recordings(current_user, Access::Core.levels_allow), AudioRecording, AudioRecording.filter_settings ) @@ -149,7 +154,7 @@ def update_status_user_check # auth is checked manually here - not sure if this is necessary or not if current_user.blank? render json: {error: 'not logged in'}.to_json, status: :unauthorized - elsif current_user.has_role? :harvester + elsif Access::Check.is_harvester?(current_user) update_status_params_check else render json: {error: 'only harvester can check uploader permissions'}.to_json, status: :forbidden diff --git a/app/controllers/bookmarks_controller.rb b/app/controllers/bookmarks_controller.rb index 9f6e9db5..a75b6d90 100644 --- a/app/controllers/bookmarks_controller.rb +++ b/app/controllers/bookmarks_controller.rb @@ -6,7 +6,7 @@ class BookmarksController < ApplicationController def index @bookmarks, constructed_options = Settings.api_response.response_index( api_filter_params, - current_user.accessible_bookmarks, + Access::Query.bookmarks_modified(current_user), Bookmark, Bookmark.filter_settings ) @@ -45,7 +45,7 @@ def destroy def filter filter_response = Settings.api_response.response_filter( api_filter_params, - current_user.accessible_bookmarks, + Access::Query.bookmarks_modified(current_user), Bookmark, Bookmark.filter_settings ) diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 7ecc42e5..451ee6ef 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -60,7 +60,7 @@ def show def authorise_custom(request_params, user) # Can't do anything if not logged in, not in user or admin role, or not confirmed - if user.blank? || (!user.has_role?(:user) && !user.has_role?(:admin)) || !user.confirmed? + if user.blank? || (!Access::Check.is_standard_user?(user) && !Access::Check.is_admin?(user)) || !user.confirmed? fail CanCan::AccessDenied, 'Anonymous users, non-admin and non-users, or unconfirmed users cannot access media.' end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 823eeb72..ecc0d7b6 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -10,7 +10,7 @@ class ProjectsController < ApplicationController def index respond_to do |format| format.html { - @projects = get_user_projects + @projects = get_user_projects.includes(:creator).references(:creator) add_breadcrumb 'Projects', projects_path } format.json { @@ -156,7 +156,7 @@ def destroy # GET /projects/request_access def new_access_request - @all_projects = current_user.inaccessible_projects + @all_projects = Access::Query.projects_inaccessible(current_user) respond_to do |format| format.html { add_breadcrumb 'Projects', projects_path @@ -205,13 +205,7 @@ def filter private def get_user_projects - if current_user.has_role? :admin - projects = Project.includes(:creator).order('lower(name) ASC') - else - projects = current_user.projects.sort { |a, b| a.name <=> b.name } - end - - projects + Access::Query.projects_accessible(current_user).order('lower(name) ASC') end def project_params diff --git a/app/controllers/public_controller.rb b/app/controllers/public_controller.rb index 58d2a043..ef40c28b 100644 --- a/app/controllers/public_controller.rb +++ b/app/controllers/public_controller.rb @@ -56,7 +56,7 @@ def website_status #storage_msg = AudioRecording.check_storage online_window = 2.hours.ago - users_online = User.where('current_sign_in_at > ? OR last_sign_in_at > ?', online_window, online_window).count + users_online = User.where('last_seen_at > ? OR current_sign_in_at > ? OR last_sign_in_at > ?', online_window, online_window, online_window).count users_total = User.count month_ago = 1.month.ago @@ -113,7 +113,7 @@ def audio_recording_catalogue #format.html format.json { - if !current_user.blank? && current_user.is_admin? + if Access::Check.is_admin?(current_user) else @@ -124,7 +124,7 @@ def audio_recording_catalogue fail CustomErrors::ItemNotFoundError, 'Project not found from audio_recording_catalogue' end - if current_user.blank? || !current_user.can_read?(project) + if current_user.blank? || Access::Check.can?(current_user, :none, project) fail CanCan::AccessDenied, 'Project access denied from audio_recording_catalogue' end end @@ -137,7 +137,7 @@ def audio_recording_catalogue end projects = Site.where(id: params[:siteId]).first.projects - if current_user.blank? || !current_user.can_read_any?(projects) + if current_user.blank? || Access::Check.can_any?(current_user, :none, projects) fail CanCan::AccessDenied, 'Site access denied from audio_recording_catalogue' end end @@ -328,10 +328,8 @@ def recent_audio_recordings if current_user.blank? @recent_audio_recordings = AudioRecording.order(order_by_coalesce).limit(7) - elsif current_user.has_role? :admin - @recent_audio_recordings = AudioRecording.includes(site: :projects).order(order_by_coalesce).limit(10) else - @recent_audio_recordings = current_user.accessible_audio_recordings.includes(site: :projects).order(order_by_coalesce).limit(10) + @recent_audio_recordings = Access::Query.audio_recordings(current_user, Access::Core.levels_allow).includes(site: :projects).order(order_by_coalesce).limit(10) end end @@ -340,11 +338,19 @@ def recent_audio_events order_by_coalesce = 'COALESCE(audio_events.updated_at, audio_events.created_at) DESC' if current_user.blank? - @recent_audio_events = AudioEvent.order(order_by_coalesce).limit(7) - elsif current_user.has_role? :admin - @recent_audio_events = AudioEvent.includes([:creator, audio_recording: {site: :projects}]).order(order_by_coalesce).limit(10) + @recent_audio_events = AudioEvent + .order(order_by_coalesce) + .limit(7) + elsif Access::Check.is_admin?(current_user) + @recent_audio_events = AudioEvent + .includes([:creator, audio_recording: {site: :projects}]) + .order(order_by_coalesce) + .limit(10) else - @recent_audio_events = current_user.accessible_audio_events.includes([:updater, audio_recording: :site]).order(order_by_coalesce).limit(10) + @recent_audio_events = Access::Query + .audio_events(current_user, Access::Core.levels_allow) + .includes([:updater, audio_recording: :site]) + .order(order_by_coalesce).limit(10) end end diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb index a976091a..83d5b8d1 100644 --- a/app/controllers/sites_controller.rb +++ b/app/controllers/sites_controller.rb @@ -205,7 +205,7 @@ def api_custom_response(site) site_hash = {} - site_hash[:project_ids] = Site.where(id: site.id).first.projects.pluck(:id) + site_hash[:project_ids] = Site.find(site.id).projects.pluck(:id) site_hash[:location_obfuscated] = site.location_obfuscated site_hash[:custom_latitude] = site.latitude site_hash[:custom_longitude] = site.longitude @@ -214,10 +214,10 @@ def api_custom_response(site) end def get_user_sites - if current_user.has_role? :admin + if Access::Check.is_admin?(current_user) sites = Site.order('lower(name) ASC') else - sites = current_user.accessible_sites + sites = Access::Query.sites(current_user, Access::Core.levels_allow) end sites diff --git a/app/controllers/user_accounts_controller.rb b/app/controllers/user_accounts_controller.rb index a9aca536..913408a7 100644 --- a/app/controllers/user_accounts_controller.rb +++ b/app/controllers/user_accounts_controller.rb @@ -5,7 +5,9 @@ class UserAccountsController < ApplicationController # GET /users # GET /users.json def index - order = 'CASE WHEN current_sign_in_at IS NULL THEN last_sign_in_at ELSE current_sign_in_at END DESC' + order = 'CASE WHEN last_seen_at IS NOT NULL THEN last_seen_at +WHEN current_sign_in_at IS NOT NULL THEN current_sign_in_at +ELSE last_sign_in_at END DESC' @users = User.order(order).all respond_to do |format| @@ -92,7 +94,7 @@ def modify_preferences # GET /user_accounts/1/projects def projects - @user_projects = @user.accessible_projects_all.uniq + @user_projects = Access::Query.projects_accessible(@user).includes(:creator).references(:creator) .order('projects.updated_at DESC') .paginate( page: paging_params[:page].blank? ? 1 : paging_params[:page], @@ -106,7 +108,7 @@ def projects # GET /user_accounts/1/bookmarks def bookmarks - @user_bookmarks = @user.accessible_bookmarks.uniq + @user_bookmarks = Access::Query.bookmarks_modified(@user) .order('bookmarks.updated_at DESC') .paginate( page: paging_params[:page].blank? ? 1 : paging_params[:page], @@ -120,7 +122,7 @@ def bookmarks # GET /user_accounts/1/audio_event_comments def audio_event_comments - @user_audio_event_comments = @user.created_audio_event_comments.includes(:audio_event).uniq + @user_audio_event_comments = Access::Query.audio_event_comments_modified(@user) .order('audio_event_comments.updated_at DESC') .paginate( page: paging_params[:page].blank? ? 1 : paging_params[:page], @@ -133,7 +135,7 @@ def audio_event_comments end def audio_events - @user_annotations = @user.accessible_audio_events.uniq + @user_annotations = Access::Query.audio_events_modified(@user).includes(:audio_recording).references(:audio_recordings) .order('audio_events.updated_at DESC') .paginate( page: paging_params[:page].blank? ? 1 : paging_params[:page], diff --git a/app/models/ability.rb b/app/models/ability.rb index de48756c..811d70f5 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -40,11 +40,11 @@ def initialize(user) # for api security endpoints can [:show, :destroy], :api_security if user.confirmed? - if user.has_role? :admin + if Access::Check.is_admin?(user) # admin abilities can :manage, :all - elsif user.has_role?(:user) && user.confirmed? + elsif Access::Check.is_standard_user?(user) # user must have read or write permission on the associated project # -------------------------------------- @@ -52,64 +52,69 @@ def initialize(user) # project # only admin can delete projects can [:show], Project do |project| - user.has_permission?(project) + Access::Check.can?(user, :reader, project) end can [:edit, :update, :update_permissions], Project do |project| - user.can_write?(project) + Access::Check.can?(user, :writer, project) end # site # only admin can delete sites can [:show, :show_shallow], Site do |site| - user.has_permission_any?(site.projects) + # can't add .includes here - it breaks when validating projects due to ActiveRecord::AssociationRelation + Access::Check.can_any?(user, :reader, site.projects) end can [:new, :create, :edit, :update], Site do |site| - user.can_write_any?(site.projects) + # can't add .includes here - it breaks when validating projects due to ActiveRecord::AssociationRelation + # .all would have worked. I tried .where(nil), that didn't work either :/ + # https://github.com/rails/rails/issues/12756 + # https://github.com/plataformatec/has_scope/issues/41 + Access::Check.can_any?(user, :writer, site.projects) end # data set can [:show, :show_shallow], Dataset do |dataset| - user.has_permission?(dataset.project) + Access::Check.can?(user, :reader, dataset.project) end can [:new, :create, :edit, :update, :destroy], Dataset do |dataset| - user.can_write?(dataset.project) + Access::Check.can?(user, :writer, dataset.project) end # job can [:show, :create], Job do |job| - user.has_permission?(job.dataset.project) + Access::Check.can?(user, :reader, job.dataset.project) end # permission # :edit and :update are not allowed # :show, :create, :delete are only used by json api can [:show, :new, :create, :destroy], Permission do |permission| - user.can_write?(permission.project) + Access::Check.can?(user, :writer, permission.project) end # audio recording can [:show], AudioRecording do |audio_recording| - user.has_permission_any?(audio_recording.site.projects) + Access::Check.can_any?(user, :reader, audio_recording.site.projects) end # audio event can [:show, :download], AudioEvent do |audio_event| - user.has_permission_any?(audio_event.audio_recording.site.projects) + Access::Check.can_any?(user, :reader, audio_event.audio_recording.site.projects) end can [:create, :edit, :update, :destroy], AudioEvent do |audio_event| - user.can_write_any?(audio_event.audio_recording.site.projects) + Access::Check.can_any?(user, :writer, audio_event.audio_recording.site.projects) end # audio event comment # anyone can view or create comments on reference audio events # anyone with read or write permissions on the project can create comments can [:show, :create, :update], AudioEventComment do |audio_event_comment| - user.has_permission_any?(audio_event_comment.audio_event.audio_recording.site.projects) || audio_event_comment.audio_event.is_reference + Access::Check.can_any?(user, :reader, audio_event_comment.audio_event.audio_recording.site.projects) || audio_event_comment.audio_event.is_reference end # bookmark can [:create], Bookmark do |bookmark| - user.has_permission_any?(bookmark.audio_recording.site.projects) + Access::Check.can_any?(user, :reader, bookmark.audio_recording.site.projects) end # script @@ -163,7 +168,7 @@ def initialize(user) # anyone can create tags can [:index, :new, :create, :show], Tag - elsif user.has_role? :harvester + elsif Access::Check.is_harvester?(user) # harvester user is used by baw-harvester and baw-workers # baw-harvester: :new, :create, :check_uploader, :update_status # baw-workers: :update diff --git a/app/models/audio_event.rb b/app/models/audio_event.rb index 950ad66a..d675a5c4 100644 --- a/app/models/audio_event.rb +++ b/app/models/audio_event.rb @@ -44,16 +44,6 @@ class AudioEvent < ActiveRecord::Base # postgres-specific scope :select_start_absolute, lambda { select('audio_recordings.recorded_date + CAST(audio_events.start_time_seconds || \' seconds\' as interval) as start_time_absolute') } scope :select_end_absolute, lambda { select('audio_recordings.recorded_date + CAST(audio_events.end_time_seconds || \' seconds\' as interval) as end_time_absolute') } - scope :check_permissions, lambda { |user| - if user.is_admin? - where('1 = 1') # don't change query - else - creator_id_check = 'projects.creator_id = ?' - permissions_check = 'permissions.user_id = ? AND permissions.level IN (\'reader\', \'writer\')' - reference_audio_event_check = 'audio_events.is_reference IS TRUE' - where("((#{creator_id_check}) OR (#{permissions_check}) OR (#{reference_audio_event_check}))", user.id, user.id) - end - } # Define filter api settings def self.filter_settings @@ -93,16 +83,7 @@ def self.filtered(user, params) # userId: int (optional) # audioRecordingId: int (optional) - #.joins(:tags, :owner, audio_recording: {site: {projects: :permissions}}) - - # eager load tags and projects - # @see http://stackoverflow.com/questions/24397640/rails-nested-includes-on-active-records - # Note that includes works with association names while references needs the actual table name. - # @see http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-includes - query = AudioEvent - .includes(:creator, :tags, audio_recording: [{site: [{projects: :permissions}]}]) - .references(:users, :tags, :audio_recordings, :sites, :projects, :permissions) - .check_permissions(user) + query = Access::Query.audio_events(user, Access::Core.levels_allow) query = AudioEvent.filter_reference(query, params) query = AudioEvent.filter_tags(query, params) @@ -243,10 +224,7 @@ def self.filter_paging_defaults end def self.csv_filter(user, filter_params) - query = AudioEvent - .includes(:creator, :tags, audio_recording: [{site: [{projects: :permissions}]}]) - .references(:users, :tags, :audio_recordings, :sites, :projects, :permissions) - .check_permissions(user) + query = Access::Query.audio_events(user, :reader) if filter_params[:project_id] query = query.where(projects: {id: (filter_params[:project_id]).to_i}) diff --git a/app/models/dataset.rb b/app/models/dataset.rb index 3d4f0d8a..5599d179 100644 --- a/app/models/dataset.rb +++ b/app/models/dataset.rb @@ -11,7 +11,7 @@ class Dataset < ActiveRecord::Base belongs_to :creator, class_name: 'User', foreign_key: :creator_id, inverse_of: :created_datasets belongs_to :updater, class_name: 'User', foreign_key: :updater_id, inverse_of: :updated_datasets belongs_to :project, inverse_of: :datasets - has_and_belongs_to_many :sites, uniq: true + has_and_belongs_to_many :sites, -> { uniq } has_many :jobs, inverse_of: :dataset #before_save :generate_dataset_result diff --git a/app/models/permission.rb b/app/models/permission.rb index cf16d222..a9df6068 100644 --- a/app/models/permission.rb +++ b/app/models/permission.rb @@ -5,7 +5,7 @@ class Permission < ActiveRecord::Base include UserChange belongs_to :project, inverse_of: :permissions - belongs_to :user + belongs_to :user, inverse_of: :permissions belongs_to :creator, class_name: 'User', foreign_key: :creator_id, inverse_of: :created_permissions belongs_to :updater, class_name: 'User', foreign_key: :updater_id, inverse_of: :updated_permissions diff --git a/app/models/project.rb b/app/models/project.rb index faaaaaff..66a92d42 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -8,13 +8,15 @@ class Project < ActiveRecord::Base belongs_to :deleter, class_name: 'User', foreign_key: :deleter_id, inverse_of: :deleted_projects has_many :permissions, inverse_of: :project - accepts_nested_attributes_for :permissions has_many :readers, -> { where("permissions.level = 'reader'").uniq }, through: :permissions, source: :user has_many :writers, -> { where("permissions.level = 'writer'").uniq }, through: :permissions, source: :user - has_and_belongs_to_many :sites, uniq: true + has_many :owners, -> { where("permissions.level = 'owner'").uniq }, through: :permissions, source: :user + has_and_belongs_to_many :sites, -> { uniq } has_many :datasets, inverse_of: :project has_many :jobs, through: :datasets + accepts_nested_attributes_for :permissions + #plugins has_attached_file :image, styles: {span4: '300x300#', span3: '220x220#', span2: '140x140#', span1: '60x60#', spanhalf: '30x30#'}, diff --git a/app/models/site.rb b/app/models/site.rb index fb935fbe..6f6db949 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -5,8 +5,8 @@ class Site < ActiveRecord::Base attr_accessor :project_ids, :custom_latitude, :custom_longitude, :location_obfuscated # relations - has_and_belongs_to_many :projects, uniq: true - has_and_belongs_to_many :datasets, uniq: true + has_and_belongs_to_many :projects, -> { uniq } + has_and_belongs_to_many :datasets, -> { uniq } has_many :audio_recordings, inverse_of: :site belongs_to :creator, class_name: 'User', foreign_key: :creator_id, inverse_of: :created_sites @@ -22,7 +22,7 @@ class Site < ActiveRecord::Base LONGITUDE_MIN = -180 LONGITUDE_MAX = 180 - JITTER_RANGE = 0.0002 + JITTER_RANGE = 0.0005 # add deleted_at and deleter_id acts_as_paranoid @@ -53,10 +53,6 @@ class Site < ActiveRecord::Base #scope :sites_in_project, lambda { |project_ids| where(Project.specified_projects, { :ids => project_ids } ) } #scope :site_projects, lambda{ |project_ids| includes(:projects).where(:projects => {:id => project_ids} ) } - def project_ids - self.projects.collect { |project| project.id } - end - # overrides getting, does not change setting def latitude value = read_attribute(:latitude) @@ -78,27 +74,43 @@ def longitude end def update_location_obfuscated(current_user) - highest_permission = current_user.highest_permission_any(self.projects.includes(:creator)) - @location_obfuscated = highest_permission < AccessLevel::OWNER + is_owner = Access::Check.can_any?(current_user, :owner, self.projects.includes(:creator)) + + # obfuscate if level is less than owner + @location_obfuscated = !is_owner end def self.add_location_jitter(value, min, max) - # truncate to 4 decimal places, then add random jitter - # that has been truncated to 5 decimal places - # http://en.wikipedia.org/wiki/Decimal_degrees#Precision - # add or subtract between ~4m - ~20m jitter - truncate_decimals_4 = 10000.0 - truncated_value = (value * truncate_decimals_4).floor / truncate_decimals_4 + # multiply by 10,000 to get to ~10m accuracy + accuracy = 10000 + multiplied = (value * accuracy).floor.to_i - truncate_decimals_5 = 100000.0 - random_jitter = rand(-Site::JITTER_RANGE..Site::JITTER_RANGE) - truncated_jitter = (random_jitter * truncate_decimals_5).floor / truncate_decimals_5 + # get a range for potential jitter - modified_value = truncated_value + truncated_jitter + max_diff =(Site::JITTER_RANGE * accuracy).floor.to_i + range_min = multiplied - max_diff + range_max = multiplied + max_diff - # ensure range is maintained (damn floating point in-exactness) - modified_value = modified_value.round(5) + # included range (inclusive range) + range = (range_min..range_max).to_a + + excluded_diff = 1 + excluded_min = multiplied - excluded_diff + excluded_max = multiplied + excluded_diff + + # excluded numbers (inclusive range) + excluded = (excluded_min..excluded_max).to_a + + # create array of available numbers with middle range excluded + available = range - excluded + + # select a random value from the available array of ints + selected = available.sample + + # round to ensure precision is maintained (damn floating point in-exactness) + # can't usually get more accurate than 5 decimal places anyway + modified_value = (selected / accuracy).round(5) # ensure range is maintained (damn floating point in-exactness) if modified_value > (value + Site::JITTER_RANGE) diff --git a/app/models/user.rb b/app/models/user.rb index cdbf4cf6..aaa68717 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -30,20 +30,21 @@ def login default_url: '/images/user/user_:style.png' # relations - # TODO tidy up user project accessing - too many ways to do the same thing - has_many :accessible_projects, through: :permissions, source: :project - has_many :readable_projects, -> { where('permissions.level = reader') }, through: :permissions, source: :project - has_many :writable_projects, -> { where('permissions.level = writer') }, through: :permissions, source: :project + # Don't include the catch-all association to permissions + #has_many :accessible_projects, through: :permissions, source: :project + has_many :readable_projects, -> { where("permissions.level = 'reader'") }, through: :permissions, source: :project + has_many :writable_projects, -> { where("permissions.level = 'writer'") }, through: :permissions, source: :project + has_many :owned_projects, -> { where("permissions.level = 'owner'") }, through: :permissions, source: :project # relations for creator, updater, deleter, and others. has_many :created_audio_events, class_name: 'AudioEvent', foreign_key: :creator_id, inverse_of: :creator has_many :updated_audio_events, class_name: 'AudioEvent', foreign_key: :updater_id, inverse_of: :updater has_many :deleted_audio_events, class_name: 'AudioEvent', foreign_key: :deleter_id, inverse_of: :deleter - has_many :created_audio_event_comments, class_name: 'AudioEventComment', foreign_key: 'creator_id', inverse_of: :creator - has_many :updated_audio_event_comments, class_name: 'AudioEventComment', foreign_key: 'updater_id', inverse_of: :updater - has_many :deleted_audio_event_comments, class_name: 'AudioEventComment', foreign_key: 'deleter_id', inverse_of: :deleter - has_many :flagged_audio_event_comments, class_name: 'AudioEventComment', foreign_key: 'flagger_id', inverse_of: :flagger + has_many :created_audio_event_comments, class_name: 'AudioEventComment', foreign_key: :creator_id, inverse_of: :creator + has_many :updated_audio_event_comments, class_name: 'AudioEventComment', foreign_key: :updater_id, inverse_of: :updater + has_many :deleted_audio_event_comments, class_name: 'AudioEventComment', foreign_key: :deleter_id, inverse_of: :deleter + has_many :flagged_audio_event_comments, class_name: 'AudioEventComment', foreign_key: :flagger_id, inverse_of: :flagger has_many :created_audio_recordings, class_name: 'AudioRecording', foreign_key: :creator_id, inverse_of: :creator has_many :updated_audio_recordings, class_name: 'AudioRecording', foreign_key: :updater_id, inverse_of: :updater @@ -56,8 +57,8 @@ def login has_many :created_bookmarks, class_name: 'Bookmark', foreign_key: :creator_id, inverse_of: :creator has_many :updated_bookmarks, class_name: 'Bookmark', foreign_key: :updater_id, inverse_of: :updater - has_many :created_datasets, -> { includes :project}, class_name: 'Dataset', foreign_key: :creator_id, inverse_of: :creator - has_many :updated_datasets, -> { includes :project}, class_name: 'Dataset', foreign_key: :updater_id, inverse_of: :updater + has_many :created_datasets, -> { includes :project }, class_name: 'Dataset', foreign_key: :creator_id, inverse_of: :creator + has_many :updated_datasets, -> { includes :project }, class_name: 'Dataset', foreign_key: :updater_id, inverse_of: :updater has_many :created_jobs, class_name: 'Job', foreign_key: :creator_id, inverse_of: :creator has_many :updated_jobs, class_name: 'Job', foreign_key: :updater_id, inverse_of: :updater @@ -119,217 +120,13 @@ def login before_save :set_rails_tz, if: Proc.new { |user| user.tzinfo_tz_changed? } - def projects - # TODO tidy up user project accessing - too many ways to do the same thing - (self.created_projects.includes(:sites, :creator) + self.accessible_projects.includes(:sites, :creator)).uniq.sort { |a, b| a.name.downcase <=> b.name.downcase } - end - - def inaccessible_projects - user_projects = self.projects.map { |project| project.id }.to_a - - query = Project.all - - unless user_projects.blank? - query = query.where('id NOT IN (?)', user_projects) - end - - query.order(:name).uniq - end - - def recently_updated_projects - accessible_projects_all.uniq.limit(10) - end - - def accessible_projects_all - # TODO tidy up user project accessing - too many ways to do the same thing - # .includes() for left outer join - # .joins for inner join - creator_id_check = 'projects.creator_id = ?' - permissions_check = '(permissions.user_id = ? AND permissions.level IN (\'reader\', \'writer\'))' - Project - .includes(:permissions, :sites, :creator) - .where("(#{creator_id_check} OR #{permissions_check})", self.id, self.id) - .references(:permissions, :sites, :creator) - .order('projects.name DESC') - end - - def accessible_sites - user_sites = self.projects.map { |project| project.sites.map { |site| site.id } }.to_a.uniq - Site.where(id: user_sites).order('sites.name DESC') - end - - def accessible_audio_events - AudioEvent - .includes(:audio_recording, :creator) - .where(audio_recording_id: accessible_audio_recordings.select(:id)) - end - - def accessible_audio_recordings - user_sites = self.projects.map { |project| - fail "One or more sites were blank: #{project.sites.inspect}" if project.sites.any? { |site| site.nil? } - - sites_array = project.site_ids - fail "One or more site ids were nil: #{sites_array.inspect}" if sites_array.any? { |id| id.nil? } - - sites_array - }.flatten - AudioRecording.where(site_id: user_sites) - end - - def accessible_comments - audio_events = AudioEvent.where(audio_recording_id: accessible_audio_recordings.select(:id)) - AudioEventComment.where(audio_event_id: audio_events) - end - - def accessible_bookmarks - Bookmark.where(creator_id: self.id) - end - - # helper methods for permission checks - - # @param [Project] project - def can_read?(project) - !get_read_permission(project).blank? || creator?(project) - end - - # @param [Project] project - def can_write?(project) - !get_write_permission(project).blank? || creator?(project) - end - - # @param [Array] projects - # @return [boolean] - def can_write_any?(projects) - projects.each do |project| - if self.can_write?(project) - return true - end - end - false - end - - # @param [Array] projects - # @return [boolean] - def can_read_any?(projects) - projects.each do |project| - if self.can_read?(project) - return true - end - end - false - end - - # @param [Project] project - def highest_permission(project) - # low to high: none, read, write, creator/owner, admin - if self.has_role? :admin - AccessLevel::ADMIN - elsif creator?(project) - AccessLevel::OWNER - elsif self.can_write? project - AccessLevel::WRITE - elsif self.can_read? project - AccessLevel::READ - else - AccessLevel::NONE - end - end - - # @param [Array] projects - def highest_permission_any(projects) - highest = 0 - projects.each do |project| - permission = self.highest_permission(project) - if permission > highest - highest = permission - end - end - highest - end - - # Check if user has any permission on given project. - # @param [Project] project - # @return [Boolean] true if user has any permission on project. - def has_permission?(project) - !get_permission(project).blank? || creator?(project) - end - - # @param [Array] projects - def has_permission_any?(projects) - projects.each do |project| - if self.has_permission?(project) - return true - end - end - false - end - - # True if this user is the creator of project. - # @param [Project] project - # @return [Boolean] - def creator?(project) - project.creator == self - end - - # True if this user is the updater of project. - # @param [Project] project - # @return [Boolean] - def updater?(project) - project.updater == self - end - - # @param [Project] project - def get_read_permission(project) - Permission.where(user_id: self.id, project_id: project.id, level: 'reader').first - end - - # @param [Project] project - def get_write_permission(project) - Permission.where(user_id: self.id, project_id: project.id, level: 'writer').first - end - - # @param [Project] project - def get_permission(project) - Permission.where(user_id: self.id, project_id: project.id).first - end - - # Get the number of projects this user has access to. - # @return [Integer] Number of projects. - def get_project_count - projects.count - end - - # Get the number of sites this user has access to. - # @return [Integer] Number of sites. - def get_site_count - projects.map { |project| project.sites.count }.reduce(0) do |result, value| - result += value - result - end - end - - # Get the number of tags this user has used. - # @return [Integer] Number of tags. - def get_tag_count - Tagging.where('audio_events_tags.creator_id = ? OR audio_events_tags.updater_id = ?', self.id, self.id).count - end - - def get_annotation_count - AudioEvent.where('audio_events.creator_id = ? OR audio_events.updater_id = ?', self.id, self.id).count - end - - def get_bookmark_count - Bookmark.where('bookmarks.creator_id = ? OR bookmarks.updater_id = ?', self.id, self.id).count - end - - def get_comment_count - AudioEventComment.where('audio_event_comments.creator_id = ? OR audio_event_comments.updater_id = ?', self.id, self.id).count - end - - # Get the last tiem this user was seen. + # Get the last time this user was seen. # @return [DateTime] Date this user was last seen def get_last_seen - self.current_sign_in_at.blank? ? self.last_sign_in_at : self.current_sign_in_at + last_seen = self.last_seen_at + last_seen = self.current_sign_in_at if last_seen.blank? + last_seen = self.last_sign_in_at if last_seen.blank? + last_seen end # Length of time this person has been a member. @@ -363,8 +160,8 @@ def self.find_first_by_auth_conditions(warden_conditions) login = conditions.delete(:login) if login where(conditions) - .where(['lower(user_name) = :value OR lower(email) = :value', {value: login.downcase}]) - .first + .where(['lower(user_name) = :value OR lower(email) = :value', {value: login.downcase}]) + .first else where(conditions).first end diff --git a/app/views/devise/registrations/edit.html.haml b/app/views/devise/registrations/edit.html.haml index e4bfa3e3..94a86eda 100644 --- a/app/views/devise/registrations/edit.html.haml +++ b/app/views/devise/registrations/edit.html.haml @@ -29,7 +29,7 @@ .form-actions = f.button :submit, 'Update' -- unless current_user.is_admin? +- unless Access::Check.is_admin?(current_user) = link_to 'Cancel my account', registration_path(resource_name), method: :delete, title: "WARNING: This will permanently delete your account", diff --git a/app/views/permissions/index.html.haml b/app/views/permissions/index.html.haml index ccba0f4a..3a9b2017 100644 --- a/app/views/permissions/index.html.haml +++ b/app/views/permissions/index.html.haml @@ -22,20 +22,20 @@ type: 'radio', name: "[user_ids][#{user.id}][permissions][level]", value: '', - checked: !user.has_permission?(@project)} + checked: Access::Check.can?(user, :none, @project)} %td %input{id: "user_#{user.id}_permissions_level_reader", type: 'radio', name: "[user_ids][#{user.id}][permissions][level]", value: 'reader', - checked: user.can_read?(@project)} + checked: Access::Check.can?(user, :reader, @project)} %td %input{id: "user_#{user.id}_permissions_level_writer", type: 'radio', name: "[user_ids][#{user.id}][permissions][level]", value: 'writer', - checked: user.can_write?(@project)} + checked: Access::Check.can?(user, :writer, @project)} .form-actions = p.submit 'Update Permissions', class: 'btn' diff --git a/app/views/projects/_project_thumb_large.html.haml b/app/views/projects/_project_thumb_large.html.haml index d6420f59..bafc9098 100644 --- a/app/views/projects/_project_thumb_large.html.haml +++ b/app/views/projects/_project_thumb_large.html.haml @@ -4,9 +4,9 @@ .caption.span7 %h4= link_to project.name, project %p= truncate(project.description, length: 50, separator: ' ') - - if current_user.can_write?(project) + - if Access::Check.can?(current_user, :writer, project) %i.project_permission_icon.fa.fa-unlock(title="You have read & write access" data-toggle='tooltip' data-placement='top') - - elsif current_user.can_read?(project) + - elsif Access::Check.can?(current_user, :reader, project) %i.project_permission_icon.fa.fa-lock(title="You have read only access" data-toggle='tooltip' data-placement='top') %ul.nav.nav-pills.nav-stacked.pull-right{style: 'margin-bottom:0'} %li diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index c512973d..8f469dfc 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -58,9 +58,10 @@ = render layout: '/shared/right_sidebar_user_times', locals: { item: @project } do = sidebar_metadata('Notes', @project.notes) = sidebar_metadata('Urn', @project.urn) - - permission = current_user.get_permission(@project) - - unless permission.blank? - = sidebar_metadata('Your access level', permission.level) + - level = Access::Query.level_project(current_user, @project) + - unless level.blank? + - level_name = Access::Core.get_level_name(level) + = sidebar_metadata('Your access level', level_name) - content_for :title, "Project #{@project.name}" diff --git a/app/views/shared/_navbar.html.haml b/app/views/shared/_navbar.html.haml index 03ffc20d..d649b363 100644 --- a/app/views/shared/_navbar.html.haml +++ b/app/views/shared/_navbar.html.haml @@ -11,11 +11,11 @@ = menu_item 'Sign up', new_user_registration_path = menu_item 'Login', new_user_session_path - else - - if current_user.is_admin? + - if Access::Check.is_admin?(current_user) = drop_down "#{current_user.user_name}" do = menu_item 'Profile', my_account_path = menu_item 'Scripts', scripts_path if can? :manage, Script - = menu_item 'Orphan Sites', sites_orphans_path if current_user.is_admin? + = menu_item 'Orphan Sites', sites_orphans_path if Access::Check.is_admin?(current_user) = menu_item 'User List', user_accounts_path if can? :index, User - else = menu_item "#{current_user.user_name}", my_account_path diff --git a/app/views/sites/show.html.haml b/app/views/sites/show.html.haml index baf384c9..93c40912 100644 --- a/app/views/sites/show.html.haml +++ b/app/views/sites/show.html.haml @@ -5,40 +5,42 @@ %h1= @site.name %p= @site.description -- duration_sum = @site.audio_recordings.sum(:duration_seconds) -- recorded_min = @site.audio_recordings.minimum(:recorded_date) -- recorded_min = recorded_min.in_time_zone(@site.rails_tz) unless @site.rails_tz.blank? -- recorded_max = @site.audio_recordings.maximum(:recorded_date) -- recorded_max = recorded_max.in_time_zone(@site.rails_tz) unless @site.rails_tz.blank? -- recorded_diff = recorded_max - recorded_min -- last_added = @site.audio_recordings.maximum(:updated_at) - %h3 Audio Recordings .row-fluid .span6 - %p - This site contains recordings from #{recorded_min.to_formatted_s(:readable_full_without_seconds)} - to #{recorded_max.to_formatted_s(:readable_full_without_seconds)}. - %p - This site covers - = distance_of_time recorded_diff - and there are recordings for - = distance_of_time duration_sum - of that time. + - if @site.audio_recordings.blank? + %p + This site does not contain any audio recordings. + - else + - duration_sum = @site.audio_recordings.sum(:duration_seconds) + - recorded_min = @site.audio_recordings.minimum(:recorded_date) + - recorded_min = recorded_min.in_time_zone(@site.rails_tz) unless @site.rails_tz.blank? + - recorded_max = @site.audio_recordings.maximum(:recorded_date) + - recorded_max = recorded_max.in_time_zone(@site.rails_tz) unless @site.rails_tz.blank? + - recorded_diff = recorded_max - recorded_min + %p + This site contains recordings from #{recorded_min.to_formatted_s(:readable_full_without_seconds)} + to #{recorded_max.to_formatted_s(:readable_full_without_seconds)}. + %p + This site covers + = distance_of_time recorded_diff + and there are recordings for + = distance_of_time duration_sum + of that time. - %ul.nav.nav-pills.nav-stacked{style: 'margin-bottom:0'} - %li.text-center - %a{href: "/visualize?siteId=#{@site.id}"} - %i.fa.fa-eye - Visualise + %ul.nav.nav-pills.nav-stacked{style: 'margin-bottom:0'} + %li.text-center + %a{href: "/visualize?siteId=#{@site.id}"} + %i.fa.fa-eye + Visualise - %h4 Recent Annotations - %ul.nav.nav-pills.nav-stacked{style: 'margin-bottom:0'} - - AudioEvent.in_site(@site).each do |ae| - %li - = link_to "#{ae.updated_at.to_formatted_s(:readable_full_without_seconds)} by #{ae.updater.user_name}", ae.get_listen_path + %h4 Recent Annotations + %ul.nav.nav-pills.nav-stacked{style: 'margin-bottom:0'} + - AudioEvent.in_site(@site).each do |ae| + %li + = link_to "#{ae.updated_at.to_formatted_s(:readable_full_without_seconds)} by #{ae.updater.nil? ? '(unknown)' : ae.updater.user_name}", ae.get_listen_path - if @site.latitude.blank? || @site.longitude.blank? .span6.map-placeholder @@ -46,31 +48,14 @@ - else .span6= gmaps(markers: {data: @site.to_gmaps4rails}, map_options: {zoom: 7, auto_zoom: true}) = yield :scripts - -#.span12 - -# %h3 Recent Audio Recordings - -# %table.table.table-striped(data-provides='rowlink') - -# %thead - -# %tr - -# %th Recording Start Date - -# %th Duration - -# %th - -# - -# %tbody - -# - @site.audio_recordings.order(recorded_date: :desc).first(10).each do |audio_recording| - -# %tr - -# %td= link_to audio_recording.recorded_date.to_formatted_s(:readable_full), "/listen/#{audio_recording.id}" - -# %td= "about #{distance_of_time_in_words(audio_recording.duration_seconds, 0, nil, {except: ['seconds']})}" - -# %td.nolink - -# = link_to "/listen/#{audio_recording.id}", target: '_self', class: 'btn btn-mini' do - -# %i{class: 'fa fa-play'} - -# Play - content_for :right_sidebar do = render layout: '/shared/right_sidebar_user_times', locals: { item: @site } do - - permission = current_user.get_permission(@project) - - unless permission.blank? - = sidebar_metadata('Your access level', permission.level) + - level = Access::Query.level_project(current_user, @project) + - unless level.blank? + - level_name = Access::Core.get_level_name(level) + = sidebar_metadata('Your access level', level_name) - content_for :title, "Project #{@project.name} | Site #{@site.name}" diff --git a/app/views/user_accounts/_sidebar_links.html.haml b/app/views/user_accounts/_sidebar_links.html.haml index 0dcd19d8..0ecea33a 100644 --- a/app/views/user_accounts/_sidebar_links.html.haml +++ b/app/views/user_accounts/_sidebar_links.html.haml @@ -4,15 +4,15 @@ %li= link_to 'Edit my profile', edit_user_registration_path, title: 'Edit your account information', data: {toggle: 'tooltip', placement: 'right'} - if !@user.blank? && current_user != @user %li= link_to 'View user profile', user_account_path(@user), title: 'View this user\'s details', data: {toggle: 'tooltip', placement: 'right'} - - if current_user.is_admin? + - if Access::Check.is_admin?(current_user) %li= link_to 'User List', user_accounts_path, title: 'View a list of all user accounts', data: {toggle: 'tooltip', placement: 'right'} - if !@user.blank? %li= link_to 'Edit user profile', edit_user_account_path(@user), title: 'Edit this user\'s details', data: {toggle: 'tooltip', placement: 'right'} - - if !@user.blank? && (current_user == @user || current_user.is_admin?) + - if !@user.blank? && (current_user == @user || Access::Check.is_admin?(current_user)) %li= link_to 'Projects', projects_user_account_path(@user), title: "Projects #{@user.user_name} can access", data: {toggle: 'tooltip', placement: 'right'} - - if !@user.blank? && (current_user == @user || current_user.is_admin?) + - if !@user.blank? && (current_user == @user || Access::Check.is_admin?(current_user)) %li= link_to 'Bookmarks', bookmarks_user_account_path(@user), title: "Bookmarks created by #{@user.user_name}", data: {toggle: 'tooltip', placement: 'right'} - if !@user.blank? %li= link_to 'Annotations', audio_events_user_account_path(@user), title: "Annotations created by #{@user.user_name}", data: {toggle: 'tooltip', placement: 'right'} - - if !@user.blank? && (current_user == @user || current_user.is_admin?) + - if !@user.blank? && (current_user == @user || Access::Check.is_admin?(current_user)) %li= link_to 'Comments', audio_event_comments_user_account_path(@user), title: "Comments by #{@user.user_name}", data: {toggle: 'tooltip', placement: 'right'} \ No newline at end of file diff --git a/app/views/user_accounts/index.html.haml b/app/views/user_accounts/index.html.haml index 036e82db..bb61f363 100644 --- a/app/views/user_accounts/index.html.haml +++ b/app/views/user_accounts/index.html.haml @@ -14,9 +14,7 @@ %tbody - @users.each do |user| - - last_seen = user.last_seen_at unless user.last_seen_at.blank? - - last_seen = user.current_sign_in_at unless user.current_sign_in_at.blank? - - last_seen = user.last_sign_in_at + - last_seen = user.get_last_seen %tr %td= link_to "#{user.user_name} (#{user.role_symbols.join(', ')})", user_account_path(user) %td= last_seen.blank? ? '?' : last_seen.iso8601 diff --git a/app/views/user_accounts/projects.html.haml b/app/views/user_accounts/projects.html.haml index 0bc294b7..b4cf7fff 100644 --- a/app/views/user_accounts/projects.html.haml +++ b/app/views/user_accounts/projects.html.haml @@ -31,7 +31,8 @@ %tr %td= link_to project.name, project_path(project) %td= project.sites.size - %td= AccessLevel.value_to_name(@user.highest_permission(project)) + - actual_level = Access::Query.level_project(@user, project) + %td= Access::Core.get_level_name(actual_level) %td.nolink = link_to project_path(project), class: 'btn btn-mini' do %i{class: 'fa fa-home'} diff --git a/app/views/user_accounts/show.html.haml b/app/views/user_accounts/show.html.haml index fc62b574..c6d6c17f 100644 --- a/app/views/user_accounts/show.html.haml +++ b/app/views/user_accounts/show.html.haml @@ -7,13 +7,13 @@ .span6{style:'text-align:center'} %p Projects: - = number_with_delimiter(@user.get_project_count) + = number_with_delimiter(Access::Query.projects_accessible(@user).count) %p Tags: - = number_with_delimiter(@user.get_tag_count, delimiter: ' ') + = number_with_delimiter(Access::Query.taggings_modified(@user).count, delimiter: ' ') %p Bookmarks: - = number_with_delimiter(@user.get_bookmark_count, delimiter: ' ') + = number_with_delimiter(Access::Query.bookmarks_modified(@user).count, delimiter: ' ') %p Last seen - last_seen = @user.get_last_seen @@ -21,13 +21,13 @@ .span6{style:'text-align:center'} %p Sites: - = number_with_delimiter(@user.get_site_count) + = number_with_delimiter(Access::Query.sites(@user, Access::Core.levels_allow).count) %p Annotations: - = number_with_delimiter(@user.get_annotation_count, delimiter: ' ') + = number_with_delimiter(Access::Query.audio_events_modified(@user).count, delimiter: ' ') %p Comments: - = number_with_delimiter(@user.get_comment_count, delimiter: ' ') + = number_with_delimiter(Access::Query.audio_event_comments_modified(@user).count, delimiter: ' ') %p Member for - signed_up_date = @user.created_at diff --git a/config/routes.rb b/config/routes.rb index 9436b767..9ca05731 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -298,7 +298,7 @@ get '/credits' => 'public#credits' # resque front end - authenticate :user, lambda { |u| !u.blank? && u.has_role?(:admin) } do + authenticate :user, lambda { |u| Access::Check.is_admin?(u) } do # add stats tab to web interface from resque-job-stats require 'resque-job-stats/server' # adds Statuses tab to web interface from resque-status diff --git a/lib/modules/access/check.rb b/lib/modules/access/check.rb new file mode 100644 index 00000000..9da9d433 --- /dev/null +++ b/lib/modules/access/check.rb @@ -0,0 +1,76 @@ +module Access + class Check + class << self + + # Is this user an admin? + # @param [User] user + # @return [Boolean] + def is_admin?(user) + return false if is_guest?(user) + user.has_role?(:admin) + end + + # Is this user a guest? A guest is a nil user object or unconfirmed user. + # @param [User] user + # @return [Boolean] + def is_guest?(user) + Access::Core.validate_user(user) + user.blank? || !user.confirmed? + end + + # Is this user a standard user? + # @param [User] user + # @return [Boolean] + def is_standard_user?(user) + return false if is_guest?(user) + user.has_role?(:user) + end + + # Is this user a harvester user? + # @param [User] user + # @return [Boolean] + def is_harvester?(user) + return false if is_guest?(user) + user.has_role?(:harvester) + end + + # Check if requested access level(s) is allowed based on actual access level(s). + # If actual is a higher level than requested, it is allowed. + # @param [Symbol, Array] requested + # @param [Symbol, Array] actual + # @return [Boolean] + def allowed?(requested, actual) + requested_array = requested.respond_to?(:each) ? Access::Core.validate_levels(requested) : [Access::Core.validate_level(requested)] + actual_array = actual.respond_to?(:each) ? Access::Core.validate_levels(actual) : [Access::Core.validate_level(actual)] + + actual_highest = Access::Core.highest(actual_array) + actual_equal_or_lower = Access::Core.equal_or_lower(actual_highest) + requested_highest = Access::Core.highest(requested_array) + + actual_equal_or_lower.include?(requested_highest) + end + + # Does this user have this access level to this project? + # @param [User] user + # @param [Symbol] level + # @param [Project] project + # @return [Boolean] + def can?(user, level, project) + can_any?(user, level, [project]) + end + + # Does this user have this access level to this project? + # @param [User] user + # @param [Symbol] level + # @param [Array] projects + # @return [Boolean] + def can_any?(user, level, projects) + requested_level = Access::Core.validate_level(level) + actual_level = Access::Query.level_projects(user, projects) + + allowed?(requested_level, actual_level) + end + + end + end +end \ No newline at end of file diff --git a/lib/modules/access/core.rb b/lib/modules/access/core.rb new file mode 100644 index 00000000..f398d206 --- /dev/null +++ b/lib/modules/access/core.rb @@ -0,0 +1,267 @@ +module Access + class Core + class << self + + # Get a hash with symbols, names, action words for the available levels. + # @return [Hash] + def levels + { + owner: { + name: 'Owner', + action: 'own' + }, + writer: { + name: 'Writer', + action: 'write' + }, + reader: { + name: 'Reader', + action: 'read' + }, + none: { + name: 'None', + action: 'no' + } + } + end + + def levels_allow + [:reader, :writer, :owner] + end + + def levels_deny + [:none] + end + + # Get a hash with symbols, names, action words for the available roles. + # @return [Hash] + def roles + { + admin: { + name: 'Administrator', + action: 'administer' + }, + user: { + name: 'User', + action: 'use' + }, + harvester: { + name: 'Harvester', + action: 'harvest' + } + } + end + + # Get level display name. + # @param [Symbol] key + # @return [string] name + def get_level_name(key) + get_hash_value(levels, key, :name) + end + + # Get level action word. + # @param [Symbol] key + # @return [string] action word + def get_level_action(key) + get_hash_value(levels, key, :action) + end + + # Get role display name. + # @param [Symbol] key + # @return [string] name + def get_role_name(key) + get_hash_value(roles, key, :name) + end + + # Get role action word. + # @param [Symbol] key + # @return [string] action word + def get_role_action(key) + get_hash_value(roles, key, :action) + end + + # Get value from hash of symbols, names, and action words. + # @param [Hash] hash + # @param [Symbol] key + # @param [Symbol] attribute + # @return [string] value + def get_hash_value(hash, key, attribute) + hash[key][attribute] + end + + # Validate access level. + # @param [Object] level + # @return [Symbol] level + def validate_level(level) + fail ArgumentError, 'Access level must not be blank.' if level.blank? + + valid_levels = levels.keys + level_sym = level.to_sym + + fail ArgumentError, "Access level '#{level_sym}' is not in available levels '#{valid_levels}'." unless valid_levels.include?(level_sym) + + level_sym + end + + # Validate array of access levels. + # @param [Array] levels + # @return [Array] levels + def validate_levels(levels) + fail ArgumentError, 'Access level array must not be blank.' if levels.blank? + levels = [levels] unless levels.respond_to?(:map) + + levels_sym = levels.map { |i| validate_level(i) }.uniq + validate_level_combination(levels_sym) + levels_sym + end + + # Validate level combination. + # @param [Array] levels + # @return [Array] levels + def validate_level_combination(levels) + if levels.respond_to?(:each) + if (levels.include?(:none) || levels.include?('none')) && levels.size > 1 + # none cannot be with other levels because this can be ambiguous, and points to a problem with how the + # permissions were obtained. + fail ArgumentError, "Level array cannot contain none with other levels, got '#{levels.join(', ')}'." + else + levels + end + else + fail ArgumentError, "Must be an array of levels, got '#{levels.class}'." + end + end + + # Validate Project. + # @param [Project] project + # @return [Project] project + def validate_project(project) + fail ArgumentError, "Project was not valid, got '#{project.class}'." if project.blank? || !project.is_a?(Project) + project + end + + # Validate Projects. + # @param [Array] projects + # @return [Array] projects + def validate_projects(projects) + projects.to_a.map { |p| validate_project(p) } + end + + # Validate User. User can be nil. + # @param [User] user + # @return [User] user + def validate_user(user) + fail ArgumentError, "User was not valid, got '#{user.class}'." if !user.blank? && !user.is_a?(User) + user + end + + # Validate Users. User can be nil. + # @param [Array] users + # @return [Array] users + def validate_users(users) + users.to_a.map { |u| validate_user(u) } + end + + # Get an array of access levels that are equal or lower. + # @param [Symbol] level + # @return [Array] + def equal_or_lower(level) + level_sym = validate_level(level) + case level_sym + when :owner + [:reader, :writer, :owner] + when :writer + [:reader, :writer] + when :reader + [:reader] + when :none + [:none] + else + fail ArgumentError, "Can not get equal or lower level for #{level}, must be one of #{values.keys.join(', ')}." + end + + end + + # Get an array of access levels that are equal or greater. + # @param [Symbol] level + # @return [Array] + def equal_or_greater(level) + level_sym = validate_level(level) + case level_sym + when :owner + [:owner] + when :writer + [:writer, :owner] + when :reader + [:reader, :writer, :owner] + when :none + [:none] + else + fail ArgumentError, "Can not get equal or greater level for #{level}, must be one of #{values.keys.join(', ')}." + end + end + + # Get the highest access level. + # @param [Array] levels + # @return [Symbol] + def highest(levels) + levels_sym = validate_levels(levels) + + return :owner if levels_sym.include?(:owner) + return :writer if levels_sym.include?(:writer) + return :reader if levels_sym.include?(:reader) + :none if levels_sym.include?(:none) + end + + # Get the lowest access level. + # @param [Array] levels + # @return [Symbol] + def lowest(levels) + levels_sym = validate_levels(levels) + + return :none if levels_sym.include?(:none) + return :reader if levels_sym.include?(:reader) + return :writer if levels_sym.include?(:writer) + :owner if levels_sym.include?(:owner) + end + + # Add project permission restrictions. + # @param [User] user + # @param [Symbol, Array] levels + # @return [ActiveRecord::Relation] modified query + def query_project_access(user, levels, query) + user = Access::Core.validate_user(user) + levels = Access::Core.validate_levels(levels) + + if Access::Check.is_admin?(user) + query + elsif Access::Check.is_standard_user?(user) + if levels == [:none] + + query + .where( + '(NOT EXISTS (SELECT 1 FROM projects invert_pt WHERE invert_pt.creator_id = ?))', + user.id) + .where( + '(NOT EXISTS (SELECT 1 FROM permissions invert_pm WHERE invert_pm.user_id = ? AND invert_pm.project_id = projects.id))', + user.id) + else + + # include reference audio_events when querying for audio_events only, at any level + check_reference_audio_events = query.model.model_name.name == 'AudioEvent' + reference_audio_events = check_reference_audio_events ? ' OR (audio_events.is_reference IS TRUE)' : '' + + query + .where("((projects.creator_id = ?) OR (permissions.user_id = ? AND permissions.level IN (?))#{reference_audio_events})", + user.id, user.id, levels) + end + else + is_guest = Access::Check.is_guest?(user) + fail ArgumentError, "User #{user.id} who is #{is_guest ? '' : 'not'} a guest with roles #{user.role_symbols.join(', ')} has no access." + end + + end + + end + end +end \ No newline at end of file diff --git a/lib/modules/access/query.rb b/lib/modules/access/query.rb new file mode 100644 index 00000000..814e17ec --- /dev/null +++ b/lib/modules/access/query.rb @@ -0,0 +1,205 @@ +module Access + class Query + class << self + + # Get access level for this user for this project (checks Permission and Project creator). + # @param [User] user + # @param [Project] project + # @return [Symbol] level + def level_project(user, project) + level_projects(user, [project]) + end + + def level_projects(user, projects) + projects = Access::Core.validate_projects(projects) + user = Access::Core.validate_user(user) + + # check based on role + if Access::Check.is_admin?(user) + :owner + elsif Access::Check.is_standard_user?(user) + creator_lvl = level_project_creators(user, projects) + permission_lvl = level_permissions(user, projects) + + levels = [permission_lvl, creator_lvl].compact + + levels.blank? ? :none : Access::Core.highest(levels) + else + # guest, harvester, or invalid role + :none + end + end + + # Get access level for this user for this project (only checks Permission). + # @param [User] user + # @param [Project] project + # @return [Symbol, nil] level + def level_permission(user, project) + project = Access::Core.validate_project(project) + user = Access::Core.validate_user(user) + + levels = Permission.where(project: project, user: user).pluck(:level) + + if levels.size > 1 + fail ActiveRecord::RecordNotUnique, "Found more than one permission matching project id #{project.id}, user id #{user.id}." + elsif levels.size < 1 + nil + else + levels[0].to_sym + end + end + + # Get access level for this user for these projects (only checks Permission). + # @param [User] user + # @param [Array] projects + # @return [Symbol, nil] level + def level_permissions(user, projects) + projects = Access::Core.validate_projects(projects) + user = Access::Core.validate_user(user) + + levels = Permission.where(project: projects, user: user).pluck(:level).map{ |l| l.to_sym } + + if levels.size < 1 + nil + else + Access::Core.highest(levels) + end + end + + # Get access level for this user for this project (only checks Project creator). + # @param [User] user + # @param [Project] project + # @return [Symbol, nil] level + def level_project_creator(user, project) + level_project_creators(user, [project]) + end + + # Get access level for this user for these project (only checks Project creator). + # @param [User] user + # @param [Array] projects + # @return [Symbol, nil] level + def level_project_creators(user, projects) + projects = Access::Core.validate_projects(projects) + user = Access::Core.validate_user(user) + + is_creator = projects.any? { |p| p.creator == user } + is_creator ? :owner : nil + end + + # Get all projects for which this user has these access levels. + # @param [User] user + # @param [Symbol, Array] levels + # @return [ActiveRecord::Relation] projects + def projects(user, levels) + user = Access::Core.validate_user(user) + levels = Access::Core.validate_levels(levels) + + # .includes eager loads tables using left outer join + # .references ensures the join is added to the sql when using sql string fragments + # .joins uses inner join + # need to use left outer join for permissions, as there might not be a permission, but the project + # should be included because the user created it + query = Project.includes(:permissions).references(:permissions) + + Access::Core.query_project_access(user, levels, query) + end + + def projects_accessible(user) + projects(user, Access::Core.levels_allow) + end + + def projects_inaccessible(user) + projects(user, Access::Core.levels_deny) + end + + # Get all sites for which this user has these access levels. + # @param [User] user + # @param [Symbol, Array] levels + # @return [ActiveRecord::Relation] sites + def sites(user, levels) + user = Access::Core.validate_user(user) + levels = Access::Core.validate_levels(levels) + + query = Site + .includes(projects: [:permissions]) + .joins(:projects) + .references(:permissions) + + Access::Core.query_project_access(user, levels, query) + end + + # Get all audio recordings for which this user has these access levels. + # @param [User] user + # @param [Symbol, Array] levels + # @return [ActiveRecord::Relation] audio recordings + def audio_recordings(user, levels) + user = Access::Core.validate_user(user) + levels = Access::Core.validate_levels(levels) + + query = AudioRecording + .includes(site: [{projects: [:permissions]}]) + .joins(site: :projects) + .references(:permissions) + + Access::Core.query_project_access(user, levels, query) + end + + # Get all audio events for which this user has this user has these access levels. + # @param [User] user + # @param [Symbol, Array] levels + # @return [ActiveRecord::Relation] audio events + def audio_events(user, levels) + user = Access::Core.validate_user(user) + levels = Access::Core.validate_levels(levels) + + # eager load tags and projects + # @see http://stackoverflow.com/questions/24397640/rails-nested-includes-on-active-records + # Note that includes works with association names while references needs the actual table name. + # @see http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-includes + query = AudioEvent + .includes([:creator, :tags, audio_recording: [{site: [{projects: [:permissions]}]}]]) + .joins(audio_recording: [site: [:projects]]) + .references(:users, :tags, :permissions) + + Access::Core.query_project_access(user, levels, query) + end + + # Get all audio event comments for which this user has this user has these access levels. + # @param [User] user + # @param [Symbol, Array] levels + # @return [ActiveRecord::Relation] audio event comments + def audio_event_comments(user, levels) + user = Access::Core.validate_user(user) + levels = Access::Core.validate_levels(levels) + + query = AudioEventComment + .includes(audio_event: [audio_recording: [site: [{projects: [:permissions]}]]]) + .joins(audio_event: [audio_recording: [site: [:projects]]]) + .references(:permissions) + + Access::Core.query_project_access(user, levels, query) + end + + def taggings_modified(user) + user = Access::Core.validate_user(user) + Tagging.where('(audio_events_tags.creator_id = ? OR audio_events_tags.updater_id = ?)', user.id, user.id) + end + + def audio_events_modified(user) + user = Access::Core.validate_user(user) + AudioEvent.where('(audio_events.creator_id = ? OR audio_events.updater_id = ?)', user.id, user.id) + end + + def bookmarks_modified(user) + user = Access::Core.validate_user(user) + Bookmark.where('(bookmarks.creator_id = ? OR bookmarks.updater_id = ?)', user.id, user.id) + end + + def audio_event_comments_modified(user) + user = Access::Core.validate_user(user) + AudioEventComment.where('(audio_event_comments.creator_id = ? OR audio_event_comments.updater_id = ?)', user.id, user.id) + end + + end + end +end \ No newline at end of file diff --git a/lib/modules/filter/custom.rb b/lib/modules/filter/custom.rb index 935379ea..8712f814 100644 --- a/lib/modules/filter/custom.rb +++ b/lib/modules/filter/custom.rb @@ -12,47 +12,6 @@ module Custom private - # Build project creator condition. - # @param [Integer] creator_id - # @return [Arel::Nodes::Node] condition - def compose_project_creator(creator_id) - # creator_id_check = 'projects.creator_id = ?' - compose_eq(relation_table(Project), :creator_id, [:creator_id], creator_id) - end - - # Build user permissions condition. - # @param [Integer] user_id - # @return [Arel::Nodes::Node] condition - def compose_user_permissions(user_id) - # permissions_check = 'permissions.user_id = ? AND permissions.level IN (\'reader\', \'writer\')' - user_permissions = compose_eq(relation_table(Permission), :user_id, [:user_id], user_id) - permission_level = compose_in(relation_table(Permission), :level, [:level], %w(reader writer)) - compose_and(user_permissions, permission_level) - end - - # Build project creator condition. - # @param [Boolean] is_reference - # @return [Arel::Nodes::Node] condition - def compose_audio_event_reference(is_reference) - # reference_audio_event_check = 'audio_events.is_reference IS TRUE' - compose_eq(relation_table(AudioEvent), :is_reference, [:is_reference], is_reference) - end - - # Build permission check condition. - # @param [Integer] user_id - # @param [Boolean] is_reference - # @return [Arel::Nodes::Node] condition - def compose_permission_check(user_id, is_reference) - # where("((#{creator_id_check}) OR (#{permissions_check}) OR (#{reference_audio_event_check}))", user.id, user.id) - compose_or( - compose_or( - compose_project_creator(user_id), - compose_user_permissions(user_id) - ), - compose_audio_event_reference(is_reference) - ) - end - # Create SIMILAR TO condition for text. # @param [Arel::Table] table # @param [Symbol] column_name diff --git a/spec/features/sites_spec.rb b/spec/features/sites_spec.rb index fdf12561..1aa3cbcf 100644 --- a/spec/features/sites_spec.rb +++ b/spec/features/sites_spec.rb @@ -23,7 +23,8 @@ end it 'creates new site when filling out form correctly' do - visit new_project_site_path(@project) + url = new_project_site_path(@project) + visit url #save_and_open_page fill_in 'site[name]', with: 'test name' fill_in 'site[description]', with: 'description' @@ -34,9 +35,10 @@ end it 'Fails to create new site when filling out form incomplete' do - visit new_project_site_path(@project) - click_button 'Create Site' + url = new_project_site_path(@project) + visit url #save_and_open_page + click_button 'Create Site' expect(page).to have_content('Please review the problems below:') end From 11c458f39542284f2c73d0ec701879e98f0363db Mon Sep 17 00:00:00 2001 From: cofiem Date: Mon, 9 Mar 2015 16:34:24 +1000 Subject: [PATCH 19/49] Small changes to project and site UI for #164 --- CHANGELOG.md | 3 + app/assets/stylesheets/projects.scss | 6 ++ app/helpers/application_helper.rb | 84 +++++++++++++++++++ app/models/audio_event.rb | 15 +--- app/models/audio_recording.rb | 4 - app/models/bookmark.rb | 8 -- app/models/site.rb | 27 +++++- .../projects/_project_thumb_large.html.haml | 12 ++- app/views/projects/show.html.haml | 2 +- app/views/public/website_status.html.haml | 4 +- app/views/sites/_site_thumb_large.html.haml | 15 +++- app/views/sites/show.html.haml | 23 +++-- .../audio_event_comments.html.haml | 6 +- .../user_accounts/audio_events.html.haml | 2 +- app/views/user_accounts/bookmarks.html.haml | 2 +- db/schema.rb | 4 +- 16 files changed, 167 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7456f75..c0708ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased + - 2015-03-09 + - More changes to project and site pages [#164] + - 2015-03-08 - Improved site lat/long obfuscation calculation - Refactored permission code to prepare for project logged in and anon permissions (this was a quite large and sweeping change) [#99](https://github.com/QutBioacoustics/baw-server/issues/99) diff --git a/app/assets/stylesheets/projects.scss b/app/assets/stylesheets/projects.scss index bd29e099..7a199673 100644 --- a/app/assets/stylesheets/projects.scss +++ b/app/assets/stylesheets/projects.scss @@ -21,6 +21,12 @@ $baseFontSize: 14px; top: 10px; font-size: $baseFontSize * 1.75; } + + ul.nav { + position: absolute; + right: 10px; + bottom: 10px; + } } select[multiple] { diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 4d482811..493047f2 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -37,4 +37,88 @@ def input(attribute_name, options = {}, &block) super end end + + # for constructing links to the angular site + def make_listen_path(value, start_offset_sec = nil, end_offset_sec = nil) + fail ArgumentError, 'Must specify a value for make_listen_path.' if value.blank? + + # obtain audio recording id + if value.is_a?(AudioRecording) + ar_id = value.id + elsif value.is_a?(AudioEvent) + ar_id = value.audio_recording_id + elsif value.is_a?(Bookmark) + ar_id = value.audio_recording_id + else + ar_id = value.to_i + end + + # obtain offsets + if value.is_a?(AudioEvent) + start_offset_sec = value.start_time_seconds if start_offset_sec.blank? + end_offset_sec = value.end_time_seconds if end_offset_sec.blank? + elsif value.is_a?(Bookmark) + start_offset_sec = value.offset_seconds if start_offset_sec.blank? + end + + start_offset_sec = end_offset_sec if start_offset_sec.blank? && !end_offset_sec.blank? + end_offset_sec = start_offset_sec if end_offset_sec.blank? && !start_offset_sec.blank? + + link = "/listen/#{ar_id}" + + if start_offset_sec.blank? && end_offset_sec.blank? + link + else + segment_duration_seconds = 30 + + offset_start_rounded = (start_offset_sec / segment_duration_seconds).floor * segment_duration_seconds + offset_end_rounded = (end_offset_sec / segment_duration_seconds).floor * segment_duration_seconds + offset_end_rounded += (offset_start_rounded == offset_end_rounded ? segment_duration_seconds : 0) + + "#{link}?start=#{offset_start_rounded}&end=#{offset_end_rounded}" + end + + end + + def make_library_path(ar_value, ae_value = nil) + fail ArgumentError, 'Must provide audio recording id.' if ar_value.blank? + + ar_id, ae_id = nil + + # obtain audio recording id + if ar_value.is_a?(AudioRecording) + ar_id = ar_value.id + elsif ar_value.is_a?(AudioEvent) + ar_id = ar_value.audio_recording_id + ae_id = ar_value.id + else + ar_id = ar_value.to_i + end + + # obtain audio event id + if ae_value.is_a?(AudioEvent) + ae_id = ae_value.id + elsif !ae_value.blank? + ae_id = ae_value.to_i + end + + fail ArgumentError, 'Must provide audio event id' if ae_id.blank? + + "/library/#{ar_id}/audio_events/#{ae_id}" + end + + def make_visualise_path(value) + fail ArgumentError, 'Must provide project or site' if value.blank? + + link = '/visualize?' + + if value.is_a?(Project) + "#{link}projectId=#{value.id}" + elsif value.is_a?(Site) + "#{link}siteId=#{value.id}" + else + fail ArgumentError, "Must provide project or site, got #{value.class}" + end + end + end diff --git a/app/models/audio_event.rb b/app/models/audio_event.rb index d675a5c4..9a50f3e6 100644 --- a/app/models/audio_event.rb +++ b/app/models/audio_event.rb @@ -249,19 +249,6 @@ def self.csv_filter(user, filter_params) query.order('audio_events.id DESC') end - def get_listen_path - segment_duration_seconds = 30 - offset_start_rounded = (self.start_time_seconds / segment_duration_seconds).floor * segment_duration_seconds - offset_end_rounded = (self.end_time_seconds / segment_duration_seconds).floor * segment_duration_seconds - offset_end_rounded += (offset_start_rounded == offset_end_rounded ? segment_duration_seconds : 0) - - "#{self.audio_recording.get_listen_path}?start=#{offset_start_rounded}&end=#{offset_end_rounded}" - end - - def get_library_path - "/library/#{self.audio_recording_id}/audio_events/#{self.id}" - end - def self.in_site(site) AudioEvent.find_by_sql(["SELECT ae.* FROM audio_events ae @@ -269,7 +256,7 @@ def self.in_site(site) INNER JOIN sites s ON ar.site_id = s.id WHERE s.id = :site_id ORDER BY ae.updated_at DESC -LIMIT 5", {site_id: site.id}]) +LIMIT 6", {site_id: site.id}]) end private diff --git a/app/models/audio_recording.rb b/app/models/audio_recording.rb index 822a3c15..1d9b2b67 100644 --- a/app/models/audio_recording.rb +++ b/app/models/audio_recording.rb @@ -218,10 +218,6 @@ def self.filter_settings } end - def get_listen_path - "/listen/#{self.id}" - end - private def set_uuid self.uuid = UUIDTools::UUID.random_create.to_s diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb index 520dc28a..1ffeb6d5 100644 --- a/app/models/bookmark.rb +++ b/app/models/bookmark.rb @@ -17,14 +17,6 @@ class Bookmark < ActiveRecord::Base validates :audio_recording_id, presence: true validates :name, presence: true, uniqueness: {case_sensitive: false, scope: :creator_id, message: 'should be unique per user'} - def get_listen_path - segment_duration_seconds = 30 - offset_start_rounded = (self.offset_seconds / segment_duration_seconds).floor * segment_duration_seconds - offset_end_rounded = offset_start_rounded + segment_duration_seconds - - "#{self.audio_recording.get_listen_path}?start=#{offset_start_rounded}&end=#{offset_end_rounded}" - end - # Define filter api settings def self.filter_settings { diff --git a/app/models/site.rb b/app/models/site.rb index 6f6db949..e85c63db 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -1,7 +1,7 @@ class Site < ActiveRecord::Base # ensures that creator_id, updater_id, deleter_id are set include UserChange - + attr_accessor :project_ids, :custom_latitude, :custom_longitude, :location_obfuscated # relations @@ -53,6 +53,31 @@ class Site < ActiveRecord::Base #scope :sites_in_project, lambda { |project_ids| where(Project.specified_projects, { :ids => project_ids } ) } #scope :site_projects, lambda{ |project_ids| includes(:projects).where(:projects => {:id => project_ids} ) } + def get_bookmark + Bookmark.where(audio_recording: self.audio_recordings).order(:updated_at).first + end + + def most_recent_recording + self.audio_recordings.order(recorded_date: :desc).first + end + + def get_bookmark_or_recording + bookmark = get_bookmark + if bookmark.blank? + { + audio_recording:most_recent_recording, + start_offset_seconds: nil, + source: :audio_recording + } + else + { + audio_recording:bookmark.audio_recording, + start_offset_seconds: bookmark.offset_seconds, + source: :bookmark + } + end + end + # overrides getting, does not change setting def latitude value = read_attribute(:latitude) diff --git a/app/views/projects/_project_thumb_large.html.haml b/app/views/projects/_project_thumb_large.html.haml index bafc9098..49b40b1c 100644 --- a/app/views/projects/_project_thumb_large.html.haml +++ b/app/views/projects/_project_thumb_large.html.haml @@ -2,14 +2,18 @@ .thumbnail.right-caption.span12.project_thumbnail .span4= image_tag project.image.url(:span2) .caption.span7 - %h4= link_to project.name, project + %h4= project.name %p= truncate(project.description, length: 50, separator: ' ') - if Access::Check.can?(current_user, :writer, project) %i.project_permission_icon.fa.fa-unlock(title="You have read & write access" data-toggle='tooltip' data-placement='top') - elsif Access::Check.can?(current_user, :reader, project) %i.project_permission_icon.fa.fa-lock(title="You have read only access" data-toggle='tooltip' data-placement='top') - %ul.nav.nav-pills.nav-stacked.pull-right{style: 'margin-bottom:0'} + %ul.nav.nav-pills{style: 'margin-bottom:0'} %li - %a{href: "/visualize?projectId=#{project.id}"} + %a{href: project_path(project)} + %i.fa.fa-info-circle + Details + %li + %a{href: make_visualise_path(project)} %i.fa.fa-eye - Visualise + Visualise \ No newline at end of file diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 8f469dfc..5f2e5137 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -80,7 +80,7 @@ -# if can? :edit, @project %li= link_to 'Add New Job', new_project_job_path(@project), title: "Add a new analysis job to this project", data: {toggle: 'tooltip', placement: 'right'} - if can? :show, @project - %li= link_to 'Visualise', "/visualize?projectId=#{@project.id}", title: 'Visualise and browse the audio in this project', data: {toggle: 'tooltip', placement: 'right'} + %li= link_to 'Visualise', make_visualise_path(@project), title: 'Browse the audio in this project', data: {toggle: 'tooltip', placement: 'right'} -# include markerwithlabel.js %script diff --git a/app/views/public/website_status.html.haml b/app/views/public/website_status.html.haml index 23e3a858..65298458 100644 --- a/app/views/public/website_status.html.haml +++ b/app/views/public/website_status.html.haml @@ -47,8 +47,8 @@ - priority_tag = Tag.get_priority_tag(tags) || Tag.new(text:'(none)') - unless current_user.blank? - site_link = "/projects/#{audio_event.audio_recording.site.projects[0].id}/sites/#{audio_event.audio_recording.site.id}" - - audio_link = audio_event.get_listen_path - - annotation_link = audio_event.get_library_path + - audio_link = make_listen_path(audio_event) + - annotation_link = make_library_path(audio_event) - if current_user.blank? %td{title: tags_text}= priority_tag.text - else diff --git a/app/views/sites/_site_thumb_large.html.haml b/app/views/sites/_site_thumb_large.html.haml index 673d31cd..25bdd56a 100644 --- a/app/views/sites/_site_thumb_large.html.haml +++ b/app/views/sites/_site_thumb_large.html.haml @@ -2,10 +2,19 @@ .thumbnail.right-caption.span12 = image_tag site.image.url(:span1) .caption - %h3= link_to site.name, [@project, site] - %ul.nav.nav-pills.nav-stacked.pull-right{style: 'margin-bottom:0'} + %h3= site.name + %ul.nav.nav-pills.pull-right{style: 'margin-bottom:0'} %li - %a{href: "/visualize?siteId=#{site.id}"} + %a{href: project_site_path(@project, site)} + %i.fa.fa-info-circle + Details + %li + - play_details = site.get_bookmark_or_recording + %a{href: make_listen_path(play_details[:audio_recording], play_details[:start_offset_seconds])} + %i.fa.fa-play-circle + Play + %li + %a{href: make_visualise_path(site)} %i.fa.fa-eye Visualise diff --git a/app/views/sites/show.html.haml b/app/views/sites/show.html.haml index 93c40912..401e5a18 100644 --- a/app/views/sites/show.html.haml +++ b/app/views/sites/show.html.haml @@ -5,6 +5,10 @@ %h1= @site.name %p= @site.description +- play_details = @site.get_bookmark_or_recording +- unless play_details[:audio_recording].blank? + - play_link = make_listen_path(play_details[:audio_recording], play_details[:start_offset_seconds]) + %h3 Audio Recordings @@ -30,9 +34,13 @@ = distance_of_time duration_sum of that time. - %ul.nav.nav-pills.nav-stacked{style: 'margin-bottom:0'} - %li.text-center - %a{href: "/visualize?siteId=#{@site.id}"} + %ul.nav.nav-pills{style: 'margin-bottom:0'} + %li + %a{href: play_link} + %i.fa.fa-play-circle + Play + %li + %a{href: make_visualise_path(@site)} %i.fa.fa-eye Visualise @@ -40,7 +48,10 @@ %ul.nav.nav-pills.nav-stacked{style: 'margin-bottom:0'} - AudioEvent.in_site(@site).each do |ae| %li - = link_to "#{ae.updated_at.to_formatted_s(:readable_full_without_seconds)} by #{ae.updater.nil? ? '(unknown)' : ae.updater.user_name}", ae.get_listen_path + - tag = ae.tags.first + - tag_text = tag.blank? ? '(not tagged)' : tag.text + - user = ae.updater.blank? ? ae.updater : ae.creator + = link_to "\"#{tag_text}\" by #{user.blank? ? '(unknown)' : user.user_name}", make_listen_path(ae) - if @site.latitude.blank? || @site.longitude.blank? .span6.map-placeholder @@ -49,7 +60,6 @@ .span6= gmaps(markers: {data: @site.to_gmaps4rails}, map_options: {zoom: 7, auto_zoom: true}) = yield :scripts - - content_for :right_sidebar do = render layout: '/shared/right_sidebar_user_times', locals: { item: @site } do - level = Access::Query.level_project(current_user, @project) @@ -71,4 +81,5 @@ %li= link_to 'Upload Audio', upload_instructions_project_site_path(@project, @site), title: "Upload new audio to this site", data: {toggle: 'tooltip', placement: 'right'} - if can? :show, @site %li= link_to 'Annotations (csv)', "#{data_request_path}?annotation_download[project_id]=#{@project.id}&annotation_download[site_id]=#{@site.id}&annotation_download[name]=#{CGI::escape(@site.name)}", title: "Download annotations for this site", data: {toggle: 'tooltip', placement: 'right'} - %li= link_to 'Visualise', "/visualize?siteId=#{@site.id}", title: 'Visualise and browse the audio in this site', data: {toggle: 'tooltip', placement: 'right'} + %li= link_to 'Visualise', make_visualise_path(@site), title: 'Visualise and browse the audio in this site', data: {toggle: 'tooltip', placement: 'right'} + %li= link_to 'Play', play_link, title: 'Play audio in this site', data: {toggle: 'tooltip', placement: 'right'} diff --git a/app/views/user_accounts/audio_event_comments.html.haml b/app/views/user_accounts/audio_event_comments.html.haml index e83ef8c9..d39ef125 100644 --- a/app/views/user_accounts/audio_event_comments.html.haml +++ b/app/views/user_accounts/audio_event_comments.html.haml @@ -23,13 +23,13 @@ %tbody - @user_audio_event_comments.each do |aec| %tr - %td= link_to aec.comment, aec.audio_event.get_library_path + %td= link_to aec.comment, make_library_path(aec.audio_event) %td= AudioEventComment.where(audio_event_id: aec.audio_event_id).count %td.nolink - = link_to aec.audio_event.get_library_path, class: 'btn btn-mini' do + = link_to make_library_path(aec.audio_event), class: 'btn btn-mini' do %i{class: 'fa fa-flag'} Annotation - = link_to aec.audio_event.get_listen_path, class: 'btn btn-mini' do + = link_to make_listen_path(aec.audio_event), class: 'btn btn-mini' do %i{class: 'fa fa-play'} Play diff --git a/app/views/user_accounts/audio_events.html.haml b/app/views/user_accounts/audio_events.html.haml index d9257231..9a73d3b2 100644 --- a/app/views/user_accounts/audio_events.html.haml +++ b/app/views/user_accounts/audio_events.html.haml @@ -23,7 +23,7 @@ - @user_annotations.each do |audio_event| - if !audio_event.blank? && !audio_event.audio_recording.blank? %tr - - audio_link = audio_event.get_listen_path + - audio_link = make_listen_path(audio_event) - audio_recording_exists = !audio_event.audio_recording.blank? - if audio_recording_exists %td= link_to (audio_event.end_time_seconds - audio_event.start_time_seconds), audio_link, target: '_self' diff --git a/app/views/user_accounts/bookmarks.html.haml b/app/views/user_accounts/bookmarks.html.haml index 35ae88c1..603e058e 100644 --- a/app/views/user_accounts/bookmarks.html.haml +++ b/app/views/user_accounts/bookmarks.html.haml @@ -24,7 +24,7 @@ %tbody - @user_bookmarks.each do |bookmark| %tr - %td= link_to bookmark.name, bookmark.get_listen_path + %td= link_to bookmark.name, make_listen_path(bookmark) %td= bookmark.category %td= bookmark.description %td.nolink diff --git a/db/schema.rb b/db/schema.rb index 631e1b79..156f4c4e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -27,8 +27,8 @@ t.integer "updater_id" t.integer "deleter_id" t.datetime "deleted_at" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "audio_events", force: :cascade do |t| From d72be6ee71aa039e2bc4f0cc64f9ceeca349c289 Mon Sep 17 00:00:00 2001 From: cofiem Date: Mon, 9 Mar 2015 23:02:08 +1000 Subject: [PATCH 20/49] updated gems, removed immigrant gem --- Gemfile | 1 - Gemfile.lock | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index ab8d01c0..e012309e 100644 --- a/Gemfile +++ b/Gemfile @@ -89,7 +89,6 @@ gem 'bcrypt', '~> 3.1.9' # don't change the database gems - causes: # Please install the adapter: `gem install activerecord--adapter` ( is not part of the bundle. Add it to Gemfile.) gem 'pg', '~> 0.18.1' -gem 'immigrant' # MODELS # ------------------------------------- diff --git a/Gemfile.lock b/Gemfile.lock index 703cb48e..a0230406 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -235,8 +235,6 @@ GEM nokogiri (~> 1.6.0) ruby_parser (~> 3.5) i18n (0.7.0) - immigrant (0.3.0) - activerecord (>= 3.0) jbuilder (2.2.11) activesupport (>= 3.0.0, < 5) multi_json (~> 1.2) @@ -253,7 +251,7 @@ GEM launchy (2.4.3) addressable (~> 2.3) libv8 (3.16.14.7) - listen (2.8.5) + listen (2.8.6) celluloid (>= 0.15.2) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) @@ -503,7 +501,6 @@ DEPENDENCIES guard-yard (~> 2.1.4) haml (~> 4.0.6) haml-rails (~> 0.8) - immigrant jbuilder (~> 2.2.3) jc-validates_timeliness (~> 3.1.1) jquery-rails (~> 4.0.3) From 3a6170fe22eef2487c97b3ef94555f336c65ec02 Mon Sep 17 00:00:00 2001 From: cofiem Date: Mon, 9 Mar 2015 23:29:04 +1000 Subject: [PATCH 21/49] last_seen_at stored in session must be a string --- app/controllers/application_controller.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f69b690a..48192863 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -60,7 +60,10 @@ class ApplicationController < ActionController::Base around_action :set_then_reset_user_stamper # update users last activity log every 10 minutes - before_action :set_last_seen_at, if: Proc.new { user_signed_in? && (session[:last_seen_at] == nil || session[:last_seen_at] < 10.minutes.ago) } + before_action :set_last_seen_at, + if: Proc.new { user_signed_in? && + (session[:last_seen_at].blank? || Time.zone.at(session[:last_seen_at].to_i) < 10.minutes.ago) + } protected @@ -480,7 +483,7 @@ def set_then_reset_user_stamper def set_last_seen_at current_user.update_attribute(:last_seen_at, Time.zone.now) - session[:last_seen_at] = Time.zone.now + session[:last_seen_at] = Time.zone.now.to_i end end From d1609a6f96623b362b1004320db0b352b19fb235 Mon Sep 17 00:00:00 2001 From: cofiem Date: Wed, 11 Mar 2015 13:51:20 +1000 Subject: [PATCH 22/49] added links to project & site name and images. closes #172 --- app/assets/stylesheets/application.scss | 4 +++- app/views/projects/_project_thumb_large.html.haml | 8 +++++--- app/views/sites/_site_thumb_large.html.haml | 5 +++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 9c0b37d0..56d59c3d 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -22,10 +22,11 @@ h3 { .caption { h3 { overflow: hidden; + } } } -.thumbnail.right-caption > img { +.thumbnail.right-caption > a img { float: left; margin-right: 9px; } @@ -38,6 +39,7 @@ h3 { padding: 0px 4px; h3 { margin: 0px; + float:left; } } diff --git a/app/views/projects/_project_thumb_large.html.haml b/app/views/projects/_project_thumb_large.html.haml index 49b40b1c..e6b622d6 100644 --- a/app/views/projects/_project_thumb_large.html.haml +++ b/app/views/projects/_project_thumb_large.html.haml @@ -1,9 +1,11 @@ %li.span6 .thumbnail.right-caption.span12.project_thumbnail - .span4= image_tag project.image.url(:span2) + .span4 + %a{href: project_path(project)} + = image_tag project.image.url(:span2), alt: project.name .caption.span7 - %h4= project.name - %p= truncate(project.description, length: 50, separator: ' ') + %h4= link_to(truncate(project.name, length: 20, separator: ' '), project_path(project), {title: project.name}) + %p= truncate(project.description, length: 40, separator: ' ') - if Access::Check.can?(current_user, :writer, project) %i.project_permission_icon.fa.fa-unlock(title="You have read & write access" data-toggle='tooltip' data-placement='top') - elsif Access::Check.can?(current_user, :reader, project) diff --git a/app/views/sites/_site_thumb_large.html.haml b/app/views/sites/_site_thumb_large.html.haml index 25bdd56a..5249d89c 100644 --- a/app/views/sites/_site_thumb_large.html.haml +++ b/app/views/sites/_site_thumb_large.html.haml @@ -1,8 +1,9 @@ %li.span12 .thumbnail.right-caption.span12 - = image_tag site.image.url(:span1) + %a{href: project_site_path(@project, site)} + = image_tag site.image.url(:span1), alt: site.name .caption - %h3= site.name + %h3= link_to truncate(site.name, length: 20, separator: ' '), project_site_path(@project, site) %ul.nav.nav-pills.pull-right{style: 'margin-bottom:0'} %li %a{href: project_site_path(@project, site)} From 2abc14b008fc0ec0b0035373119d78cc98e55218 Mon Sep 17 00:00:00 2001 From: cofiem Date: Wed, 11 Mar 2015 13:54:23 +1000 Subject: [PATCH 23/49] change default items per page to 25 from 500. Closes #171 --- lib/modules/filter/query.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/filter/query.rb b/lib/modules/filter/query.rb index 69da151f..73ff3e05 100644 --- a/lib/modules/filter/query.rb +++ b/lib/modules/filter/query.rb @@ -22,7 +22,7 @@ def initialize(parameters, query, model, filter_settings) # might need this at some point: Rack::Utils.parse_nested_query @key_prefix = 'filter_' @default_page = 1 - @default_items = 500 + @default_items = 25 @max_items = 500 @table = relation_table(model) @initial_query = !query.nil? && query.is_a?(ActiveRecord::Relation) ? query : relation_all(model) From 675d58ce581b495ea69937e7948abbf0685d9306 Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 13 Mar 2015 11:55:32 +1000 Subject: [PATCH 24/49] fixed linking to listen page when no audio in site, closes #173 --- app/views/sites/_site_thumb_large.html.haml | 15 ++++++++++----- app/views/sites/show.html.haml | 20 +++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/app/views/sites/_site_thumb_large.html.haml b/app/views/sites/_site_thumb_large.html.haml index 5249d89c..bb3896e1 100644 --- a/app/views/sites/_site_thumb_large.html.haml +++ b/app/views/sites/_site_thumb_large.html.haml @@ -9,11 +9,16 @@ %a{href: project_site_path(@project, site)} %i.fa.fa-info-circle Details - %li - - play_details = site.get_bookmark_or_recording - %a{href: make_listen_path(play_details[:audio_recording], play_details[:start_offset_seconds])} - %i.fa.fa-play-circle - Play + - play_details = site.get_bookmark_or_recording + - unless play_details.nil? + %li + %a{href: make_listen_path(play_details[:audio_recording], play_details[:start_offset_seconds])} + %i.fa.fa-play-circle + Play + - else + %li + %a{href: '#', title: 'No audio recordings in this site', data: {toggle: 'tooltip', placement: 'top'}} + No audio %li %a{href: make_visualise_path(site)} %i.fa.fa-eye diff --git a/app/views/sites/show.html.haml b/app/views/sites/show.html.haml index 401e5a18..3a51f546 100644 --- a/app/views/sites/show.html.haml +++ b/app/views/sites/show.html.haml @@ -6,8 +6,7 @@ %p= @site.description - play_details = @site.get_bookmark_or_recording -- unless play_details[:audio_recording].blank? - - play_link = make_listen_path(play_details[:audio_recording], play_details[:start_offset_seconds]) +- play_link = play_details.blank? ? nil : make_listen_path(play_details[:audio_recording], play_details[:start_offset_seconds]) %h3 Audio Recordings @@ -35,10 +34,16 @@ of that time. %ul.nav.nav-pills{style: 'margin-bottom:0'} - %li - %a{href: play_link} - %i.fa.fa-play-circle - Play + - if play_link.blank? + %li + %a{href: '#', title: 'No audio recordings in this site', data: {toggle: 'tooltip', placement: 'top'}} + No audio + - else + %li + %a{href: play_link} + %i.fa.fa-play-circle + Play + %li %a{href: make_visualise_path(@site)} %i.fa.fa-eye @@ -82,4 +87,5 @@ - if can? :show, @site %li= link_to 'Annotations (csv)', "#{data_request_path}?annotation_download[project_id]=#{@project.id}&annotation_download[site_id]=#{@site.id}&annotation_download[name]=#{CGI::escape(@site.name)}", title: "Download annotations for this site", data: {toggle: 'tooltip', placement: 'right'} %li= link_to 'Visualise', make_visualise_path(@site), title: 'Visualise and browse the audio in this site', data: {toggle: 'tooltip', placement: 'right'} - %li= link_to 'Play', play_link, title: 'Play audio in this site', data: {toggle: 'tooltip', placement: 'right'} + - unless play_link.blank? + %li= link_to 'Play', play_link, title: 'Play audio in this site', data: {toggle: 'tooltip', placement: 'right'} From da4c7d78514c89d5271ad6bbce6d7688224ae620 Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 13 Mar 2015 11:57:13 +1000 Subject: [PATCH 25/49] updated how listen links are created on audio_events and bookmark list pages --- app/views/user_accounts/audio_events.html.haml | 2 +- app/views/user_accounts/bookmarks.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/user_accounts/audio_events.html.haml b/app/views/user_accounts/audio_events.html.haml index 9a73d3b2..40fcdc09 100644 --- a/app/views/user_accounts/audio_events.html.haml +++ b/app/views/user_accounts/audio_events.html.haml @@ -36,7 +36,7 @@ = link_to audio_link, target: '_self', class: 'btn btn-mini' do %i{class: 'fa fa-play'} Play - = link_to audio_event.get_library_path, target: '_self', class: 'btn btn-mini' do + = link_to make_library_path(audio_event), target: '_self', class: 'btn btn-mini' do %i{class: 'fa fa-flag'} Annotation diff --git a/app/views/user_accounts/bookmarks.html.haml b/app/views/user_accounts/bookmarks.html.haml index 603e058e..b3502afb 100644 --- a/app/views/user_accounts/bookmarks.html.haml +++ b/app/views/user_accounts/bookmarks.html.haml @@ -28,7 +28,7 @@ %td= bookmark.category %td= bookmark.description %td.nolink - = link_to bookmark.get_listen_path, target: '_self', class: 'btn btn-mini' do + = link_to make_listen_path(bookmark), target: '_self', class: 'btn btn-mini' do %i{class: 'fa fa-play'} Play From 21b5a8a7edcddf2d5dba057e761bff83657e9c61 Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 13 Mar 2015 11:58:35 +1000 Subject: [PATCH 26/49] included Projection module instead of ad-hoc query creation --- lib/modules/filter/build.rb | 13 ++++++++++--- lib/modules/filter/query.rb | 3 +++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/modules/filter/build.rb b/lib/modules/filter/build.rb index badbe43b..193e78f4 100644 --- a/lib/modules/filter/build.rb +++ b/lib/modules/filter/build.rb @@ -264,6 +264,7 @@ def build_generic(filter_hash, table, valid_fields) # Build projections from a hash. # @param [Hash] hash + # @param [Arel::Table] table # @param [Array] valid_fields # @return [Array] projections def build_projections(hash, table, valid_fields) @@ -279,6 +280,7 @@ def build_projections(hash, table, valid_fields) # Build projection to include or exclude. # @param [Symbol] key # @param [Hash] value + # @param [Arel::Table] table # @param [Array] valid_fields # @return [Array] projections def build_projection(key, value, table, valid_fields) @@ -298,12 +300,17 @@ def build_projection(key, value, table, valid_fields) end columns.map { |item| - #project_column(table, item, valid_fields) - validate_table_column(table, item, valid_fields) - table[item] + project_column(table, item, valid_fields) } end + # Build special project ids 'in' filter. + # @param [Symbol] field + # @param [Symbol] filter_name + # @param [Object] filter_value + # @param [Arel::Table] table + # @param [Array] valid_fields + # @return [Arel::Nodes::Node] condition def build_condition_special(field, filter_name, filter_value, table, valid_fields) # construct special conditions if table.name == 'sites' && field == :project_ids diff --git a/lib/modules/filter/query.rb b/lib/modules/filter/query.rb index 73ff3e05..8b49b452 100644 --- a/lib/modules/filter/query.rb +++ b/lib/modules/filter/query.rb @@ -4,6 +4,7 @@ class Query include Comparison include Core include Subset + include Projection include Parse include Build include Validate @@ -25,6 +26,8 @@ def initialize(parameters, query, model, filter_settings) @default_items = 25 @max_items = 500 @table = relation_table(model) + + # `.all' adds 'id' to the select!! @initial_query = !query.nil? && query.is_a?(ActiveRecord::Relation) ? query : relation_all(model) @valid_fields = filter_settings[:valid_fields].map(&:to_sym) @text_fields = filter_settings[:text_fields].map(&:to_sym) From 05e06661388eb7587e0ce7a21bda8483b5773e94 Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 13 Mar 2015 12:10:10 +1000 Subject: [PATCH 27/49] fixed accessing recent recording or bookmark --- app/models/site.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/models/site.rb b/app/models/site.rb index e85c63db..07e4fee6 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -63,18 +63,21 @@ def most_recent_recording def get_bookmark_or_recording bookmark = get_bookmark - if bookmark.blank? + recording = most_recent_recording + if !bookmark.blank? + { + audio_recording:bookmark.audio_recording, + start_offset_seconds: bookmark.offset_seconds, + source: :bookmark + } + elsif !recording.blank? { audio_recording:most_recent_recording, start_offset_seconds: nil, source: :audio_recording } else - { - audio_recording:bookmark.audio_recording, - start_offset_seconds: bookmark.offset_seconds, - source: :bookmark - } + nil end end From 754caacbe6a9db9aae07098e638bc61d5b97400d Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 13 Mar 2015 12:11:53 +1000 Subject: [PATCH 28/49] update tests for filer paging, addresses #171 --- spec/lib/filter/query_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/filter/query_spec.rb b/spec/lib/filter/query_spec.rb index dffb2ec1..a4782b2c 100644 --- a/spec/lib/filter/query_spec.rb +++ b/spec/lib/filter/query_spec.rb @@ -337,7 +337,7 @@ def create_filter(params) FROM\"audio_recordings\" \ WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ AND\"audio_recordings\".\"site_id\"=5 \ -ORDERBY\"audio_recordings\".\"recorded_date\"DESCLIMIT500OFFSET0" +ORDERBY\"audio_recordings\".\"recorded_date\"DESCLIMIT25OFFSET0" compare_filter_sql(request_body_obj, complex_result) end @@ -362,7 +362,7 @@ def create_filter(params) FROM\"audio_recordings\" \ WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ AND\"audio_recordings\".\"site_id\"=5 \ -ORDERBY\"audio_recordings\".\"recorded_date\"DESCLIMIT500OFFSET0" +ORDERBY\"audio_recordings\".\"recorded_date\"DESCLIMIT25OFFSET0" compare_filter_sql(request_body_obj, complex_result) end From 4ed2277330cbdcc6ec13839e42cd491c66006a81 Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 13 Mar 2015 12:13:39 +1000 Subject: [PATCH 29/49] Updated specs to enable checking for multiple invalid content items. Added checks for filter endpoint returning unwanted attributes #170 --- spec/acceptance/audio_recordings_spec.rb | 5 ++++- spec/acceptance/sites_spec.rb | 4 ++-- spec/helpers/acceptance_spec_helper.rb | 13 ++++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/spec/acceptance/audio_recordings_spec.rb b/spec/acceptance/audio_recordings_spec.rb index 1a9dcbc5..6d72321a 100644 --- a/spec/acceptance/audio_recordings_spec.rb +++ b/spec/acceptance/audio_recordings_spec.rb @@ -899,7 +899,10 @@ def test_overlap {"orderBy" => "createdAt", "direction" => "desc"}} .to_json } let(:authentication_token) { reader_token } - standard_request_options(:post, 'FILTER (as reader with paging, sorting, projection)', :ok, {expected_json_path: 'meta/paging/current', data_item_count: 1}) + standard_request_options(:post, 'FILTER (as reader with paging, sorting, projection)', :ok, { + expected_json_path: 'meta/paging/current', + data_item_count: 1, + invalid_content: ['"status":', '"notes"']}) end end \ No newline at end of file diff --git a/spec/acceptance/sites_spec.rb b/spec/acceptance/sites_spec.rb index dda303f9..e69e6d49 100644 --- a/spec/acceptance/sites_spec.rb +++ b/spec/acceptance/sites_spec.rb @@ -290,7 +290,7 @@ data_item_count: 1, regex_match: /"project_ids"\:\[[0-9]+\]/, response_body_content: "\"project_ids\":[", - invalid_content: "\"project_ids\":[{\"id\":" + invalid_content: ["\"project_ids\":[{\"id\":", '"description":'] }) end @@ -312,7 +312,7 @@ data_item_count: 1, regex_match: /"project_ids"\:\[[0-9]+\]/, response_body_content: "\"project_ids\":[", - invalid_content: "\"project_ids\":[{\"id\":" + invalid_content: ["\"project_ids\":[{\"id\":", '"description":'] }) end diff --git a/spec/helpers/acceptance_spec_helper.rb b/spec/helpers/acceptance_spec_helper.rb index 08ec8bc0..c6780a16 100644 --- a/spec/helpers/acceptance_spec_helper.rb +++ b/spec/helpers/acceptance_spec_helper.rb @@ -290,7 +290,18 @@ def acceptance_checks_json(opts = {}) expect(actual_response_parsed_size).to eq(opts[:data_item_count]), "#{message_prefix} count to be #{opts[:data_item_count]} but got #{actual_response_parsed_size} items in #{opts[:actual_response]} (type #{data_format})" unless opts[:data_item_count].blank? expect(opts[:actual_response]).to include(opts[:response_body_content]), "#{message_prefix} to find '#{opts[:response_body_content]}' in '#{opts[:actual_response]}'" unless opts[:response_body_content].blank? - expect(opts[:actual_response]).to_not include(opts[:invalid_content]), "#{message_prefix} not to find '#{opts[:response_body_content]}' in '#{opts[:actual_response]}'" unless opts[:invalid_content].blank? + + unless opts[:invalid_content].blank? + if opts[:invalid_content].respond_to?(:each) + opts[:invalid_content].each do |invalid_content_item| + expect(opts[:actual_response]).to_not include(invalid_content_item), "#{message_prefix} not to find '#{invalid_content_item}' in '#{opts[:actual_response]}'" + end + else + expect(opts[:actual_response]).to_not include(opts[:invalid_content]), "#{message_prefix} not to find '#{opts[:invalid_content]}' in '#{opts[:actual_response]}'" + end + + end + unless opts[:expected_json_path].blank? From 60984fe115bb3ffb7f666d8f01939e7b00c11085 Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 13 Mar 2015 12:21:20 +1000 Subject: [PATCH 30/49] updated gems --- CHANGELOG.md | 32 +++++++++++++++++++------------- Gemfile.lock | 18 +++++++++--------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0708ea9..c92ac749 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,35 +2,41 @@ ## Unreleased + - 2015-03-13 + - Small UI bug fixes + - Added links to play audio and visualise projects and sites [#164](https://github.com/QutBioacoustics/baw-server/issues/164) [#172](https://github.com/QutBioacoustics/baw-server/issues/172) + - Changed filter default items per page to 25 [#171](https://github.com/QutBioacoustics/baw-server/issues/171) + - added ability to opt out of filter paging [#160](https://github.com/QutBioacoustics/baw-server/issues/160) + - 2015-03-09 - - More changes to project and site pages [#164] + - More changes to project and site pages [#164](https://github.com/QutBioacoustics/baw-server/issues/164) - 2015-03-08 - Improved site lat/long obfuscation calculation - Refactored permission code to prepare for project logged in and anon permissions (this was a quite large and sweeping change) [#99](https://github.com/QutBioacoustics/baw-server/issues/99) - 2015-03-07 - - Added last_seen_at column for users [#167] - - Added ability to disable paging for filters and enforced max item count [#160] - - Added foreign keys [#151] - - Added Timezone setting for sites and users [#116] - - Added links to visualise page [#155] - - Modify site show page to remove audio recording list [#164] + - Added last_seen_at column for users [#167](https://github.com/QutBioacoustics/baw-server/issues/167) + - Added ability to disable paging for filters and enforced max item count [#160](https://github.com/QutBioacoustics/baw-server/issues/160) + - Added foreign keys [#151](https://github.com/QutBioacoustics/baw-server/issues/151) + - Added Timezone setting for sites and users [#116](https://github.com/QutBioacoustics/baw-server/issues/116) + - Added links to visualise page [#155](https://github.com/QutBioacoustics/baw-server/issues/155) + - Modify site show page to remove audio recording list [#164](https://github.com/QutBioacoustics/baw-server/issues/164) ## [Release 0.13.1](https://github.com/QutBioacoustics/baw-server/releases/tag/0.13.1) (2015-03-02) - 2015-03-06 - Fix: ignored ffmpeg warning for channel layout - Fix: case insensitive compare when chaning audio event tags - - Added admin-only page to fix orphaned audio recordings [#153] + - Added admin-only page to fix orphaned audio recordings [#153](https://github.com/QutBioacoustics/baw-server/issues/153) - 2015-02-28 - Fix: site paging - - Fix: Admin changing user's email [#158] - - Enhancement: additional user info for Admin [#159] + - Fix: Admin changing user's email [#158](https://github.com/QutBioacoustics/baw-server/issues/158) + - Enhancement: additional user info for Admin [#159](https://github.com/QutBioacoustics/baw-server/issues/159) - Fix: error accessing sites/filter when logged in as Admin - - Enhancement: added tests to ensure numbers in json are not quoted [#152] + - Enhancement: added tests to ensure numbers in json are not quoted [#152](https://github.com/QutBioacoustics/baw-server/issues/152) ## [Release 0.13.0](https://github.com/QutBioacoustics/baw-server/releases/tag/0.13.0) (2015-02-20) @@ -41,7 +47,7 @@ - added rake task to export audio recordings to csv - 2015-01-24 - - Fixed annotation library not filtering using query string parameters [#148] + - Fixed annotation library not filtering using query string parameters [#148](https://github.com/QutBioacoustics/baw-server/issues/148) - 2015-01-18 - delete actions improved, archive headers added and fixed @@ -49,7 +55,7 @@ - added audio event filter action - 2015-01-06 - - Fixed CORS responses [#140] + - Fixed CORS responses [#140](https://github.com/QutBioacoustics/baw-server/issues/140) - Added ability to poll Resque for job completion rather than polling filesystem. - Bug fix: added more strict validation and more tests for 'in' filter. diff --git a/Gemfile.lock b/Gemfile.lock index a0230406..4570afe8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -171,7 +171,7 @@ GEM thor (~> 0.19.1) crack (0.4.2) safe_yaml (~> 1.0.0) - daemons (1.1.9) + daemons (1.2.1) database_cleaner (1.3.0) devise (3.4.1) bcrypt (~> 3.0) @@ -221,10 +221,10 @@ GEM yard (>= 0.7.0) haml (4.0.6) tilt - haml-rails (0.8.2) + haml-rails (0.9.0) actionpack (>= 4.0.1) activesupport (>= 4.0.1) - haml (>= 3.1, < 5.0) + haml (>= 4.0.6, < 5.0) html2haml (>= 1.0.1) railties (>= 4.0.1) hike (1.2.3) @@ -251,7 +251,7 @@ GEM launchy (2.4.3) addressable (~> 2.3) libv8 (3.16.14.7) - listen (2.8.6) + listen (2.9.0) celluloid (>= 0.15.2) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) @@ -321,7 +321,7 @@ GEM activesupport (>= 4.2.0.beta, < 5.0) nokogiri (~> 1.6.0) rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.1) + rails-html-sanitizer (1.0.2) loofah (~> 2.0) rails-i18n-debug (1.0.1) railties (4.2.0) @@ -360,7 +360,7 @@ GEM rspec-core (~> 3.2.0) rspec-expectations (~> 3.2.0) rspec-mocks (~> 3.2.0) - rspec-core (3.2.1) + rspec-core (3.2.2) rspec-support (~> 3.2.0) rspec-expectations (3.2.0) diff-lcs (>= 1.2.0, < 2.0) @@ -382,7 +382,7 @@ GEM json (~> 1.4, >= 1.4.6) mustache (~> 0.99, >= 0.99.4) rspec (>= 3.0.0) - ruby_parser (3.6.4) + ruby_parser (3.6.5) sexp_processor (~> 4.1) safe_yaml (1.0.4) sass (3.4.13) @@ -396,7 +396,7 @@ GEM json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) settingslogic (2.0.9) - sexp_processor (4.4.5) + sexp_processor (4.5.0) shellany (0.0.1) shoulda-matchers (2.8.0) activesupport (>= 3.0.0) @@ -437,7 +437,7 @@ GEM eventmachine (~> 1.0) rack (~> 1.0) thor (0.19.1) - thread_safe (0.3.4) + thread_safe (0.3.5) tilt (1.4.1) timeliness (0.3.7) timers (4.0.1) From 884dc24dd7c4414c7d69478d19515eb6a557916a Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 13 Mar 2015 12:29:40 +1000 Subject: [PATCH 31/49] added test for badly formatted paging link, addresses #169 --- spec/acceptance/audio_recordings_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/acceptance/audio_recordings_spec.rb b/spec/acceptance/audio_recordings_spec.rb index 6d72321a..78c061b4 100644 --- a/spec/acceptance/audio_recordings_spec.rb +++ b/spec/acceptance/audio_recordings_spec.rb @@ -902,7 +902,9 @@ def test_overlap standard_request_options(:post, 'FILTER (as reader with paging, sorting, projection)', :ok, { expected_json_path: 'meta/paging/current', data_item_count: 1, - invalid_content: ['"status":', '"notes"']}) + invalid_content: ['"status":', '"notes"'], + response_body_content: 'http://localhost:3000/audio_recordings/filter?items=10\u0026page=1\u0026direction=desc\u0026orderBy=createdAt' + }) end end \ No newline at end of file From 3e4778468220510b4e23c0eb9dfe94e52b84b5cf Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 13 Mar 2015 13:02:49 +1000 Subject: [PATCH 32/49] allow access to reference annotations from audio_events_comments controller. Closes #154 --- lib/modules/access/core.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/modules/access/core.rb b/lib/modules/access/core.rb index f398d206..4950fec6 100644 --- a/lib/modules/access/core.rb +++ b/lib/modules/access/core.rb @@ -248,7 +248,8 @@ def query_project_access(user, levels, query) else # include reference audio_events when querying for audio_events only, at any level - check_reference_audio_events = query.model.model_name.name == 'AudioEvent' + model_name = query.model.model_name.name + check_reference_audio_events = (model_name == 'AudioEvent' || model_name == 'AudioEventComment') reference_audio_events = check_reference_audio_events ? ' OR (audio_events.is_reference IS TRUE)' : '' query From ab5c65d8306c817814774c135322cfc017bedabc Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 13 Mar 2015 13:28:56 +1000 Subject: [PATCH 33/49] Made need to confirm (by chekcing email) more obvious. Closes #149 --- app/views/devise/registrations/edit.html.haml | 7 ------- app/views/layouts/application.html.haml | 8 ++++++++ app/views/public/index.html.haml | 7 ------- config/locales/devise.en.yml | 2 +- lib/modules/access/core.rb | 5 ++++- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/app/views/devise/registrations/edit.html.haml b/app/views/devise/registrations/edit.html.haml index 94a86eda..4b957cc2 100644 --- a/app/views/devise/registrations/edit.html.haml +++ b/app/views/devise/registrations/edit.html.haml @@ -1,10 +1,3 @@ -- unless current_user.blank? - - if !current_user.confirmed? - .alert.fade.in.alert-warning - %button.close{ 'data-dismiss' => 'alert'} × - =t 'devise.failure.unconfirmed' - = link_to 'Resend confirmation email', new_user_confirmation_path - %h2 Edit #{resource_name.to_s.humanize} = simple_form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { class: 'form-horizontal', :multipart => true, :method => :put }) do |f| diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 54e50e0f..1ce629f4 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -29,6 +29,14 @@ .span8 = render_breadcrumbs = bootstrap_flash + + - unless current_user.blank? + - if !current_user.confirmed? + .alert.fade.in.alert-warning + %button.close{ 'data-dismiss' => 'alert'} × + = t 'devise.failure.unconfirmed' + = link_to 'Resend confirmation email', new_user_confirmation_path + .row-fluid = yield .span2 diff --git a/app/views/public/index.html.haml b/app/views/public/index.html.haml index 9bde1267..be29b12e 100644 --- a/app/views/public/index.html.haml +++ b/app/views/public/index.html.haml @@ -1,10 +1,3 @@ -- unless current_user.blank? - - if !current_user.confirmed? - .alert.fade.in.alert-warning - %button.close{ 'data-dismiss' => 'alert'} × - =t 'devise.failure.unconfirmed' - = link_to 'Resend confirmation email', new_user_confirmation_path - %h1= Settings.organisation_names.site_long_name %p diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 6d04c528..8a2a13c0 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -15,7 +15,7 @@ en: not_found_in_database: "Invalid login or password." timeout: "Your session expired, please sign in again to continue." unauthenticated: "You need to sign in or sign up before continuing." - unconfirmed: "You have to confirm your account before continuing." + unconfirmed: "You have to confirm your account before continuing. Please check your email (including junk mail folder)." unauthorized: "You do not have sufficient permissions to access this page." mailer: confirmation_instructions: diff --git a/lib/modules/access/core.rb b/lib/modules/access/core.rb index 4950fec6..6f713372 100644 --- a/lib/modules/access/core.rb +++ b/lib/modules/access/core.rb @@ -258,7 +258,10 @@ def query_project_access(user, levels, query) end else is_guest = Access::Check.is_guest?(user) - fail ArgumentError, "User #{user.id} who is #{is_guest ? '' : 'not'} a guest with roles #{user.role_symbols.join(', ')} has no access." + Rails.logger.warn "User '#{user.user_name}' (#{user.id}) who is#{is_guest ? '' : ' not'} a guest with roles '#{user.role_symbols.join(', ')}' has no access." + + # any other role has no access (using .none to be chainable) + query.none end end From 417cf9bb62a3d8eba861ee641fe1bc0b73eb9f9a Mon Sep 17 00:00:00 2001 From: cofiem Date: Fri, 13 Mar 2015 17:21:54 +1000 Subject: [PATCH 34/49] update changelog --- CHANGELOG.md | 3 ++- lib/modules/access/core.rb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c92ac749..80c20dba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,13 @@ - Added links to play audio and visualise projects and sites [#164](https://github.com/QutBioacoustics/baw-server/issues/164) [#172](https://github.com/QutBioacoustics/baw-server/issues/172) - Changed filter default items per page to 25 [#171](https://github.com/QutBioacoustics/baw-server/issues/171) - added ability to opt out of filter paging [#160](https://github.com/QutBioacoustics/baw-server/issues/160) + - Made it more obvious that confirming an account involves checking email [#149](https://github.com/QutBioacoustics/baw-server/issues/149) - 2015-03-09 - More changes to project and site pages [#164](https://github.com/QutBioacoustics/baw-server/issues/164) - 2015-03-08 - - Improved site lat/long obfuscation calculation + - Improved site lat/long obfuscation calculation [#91](https://github.com/QutBioacoustics/baw-server/issues/91) - Refactored permission code to prepare for project logged in and anon permissions (this was a quite large and sweeping change) [#99](https://github.com/QutBioacoustics/baw-server/issues/99) - 2015-03-07 diff --git a/lib/modules/access/core.rb b/lib/modules/access/core.rb index 6f713372..d27746c5 100644 --- a/lib/modules/access/core.rb +++ b/lib/modules/access/core.rb @@ -260,7 +260,7 @@ def query_project_access(user, levels, query) is_guest = Access::Check.is_guest?(user) Rails.logger.warn "User '#{user.user_name}' (#{user.id}) who is#{is_guest ? '' : ' not'} a guest with roles '#{user.role_symbols.join(', ')}' has no access." - # any other role has no access (using .none to be chainable) + # any other role has no access (using .none to be chain-able) query.none end From 71b629434696b0d7e0957d0857a0d4fd2775a71f Mon Sep 17 00:00:00 2001 From: cofiem Date: Sat, 14 Mar 2015 12:36:02 +1000 Subject: [PATCH 35/49] updated gems --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4570afe8..5e789222 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -197,7 +197,7 @@ GEM railties (>= 3.0.0) fakeredis (0.5.0) redis (~> 3.0) - ffi (1.9.6) + ffi (1.9.7) formatador (0.2.5) globalid (0.3.3) activesupport (>= 4.1.0) From 5227bf93d6bb97b4a496dc3fb669860291f00df4 Mon Sep 17 00:00:00 2001 From: cofiem Date: Mon, 16 Mar 2015 01:09:45 +1000 Subject: [PATCH 36/49] Fixes to filer so that options are obeyed. Closes #170 closes #169 This introduces major problems in the audio event library endpoints. Considered acceptable losses for now. This end point really needs to be rewritten to be a standard filter instead. --- Gemfile.lock | 4 +- .../audio_event_comments_controller.rb | 10 +- app/controllers/bookmarks_controller.rb | 2 +- app/controllers/projects_controller.rb | 4 +- app/controllers/sites_controller.rb | 10 +- app/models/audio_event.rb | 4 +- config/application.rb | 7 +- lib/modules/access/core.rb | 28 +++- lib/modules/access/query.rb | 22 +-- lib/modules/api/controller_helper.rb | 4 +- lib/modules/api/response.rb | 140 +++++++++--------- lib/modules/filter/build.rb | 6 +- lib/modules/filter/custom.rb | 18 +++ lib/modules/filter/parse.rb | 24 +-- lib/modules/filter/query.rb | 2 +- lib/modules/filter/subset.rb | 7 +- lib/modules/filter/validate.rb | 48 ++---- spec/acceptance/audio_recordings_spec.rb | 75 +++++++++- spec/acceptance/projects_spec.rb | 20 ++- spec/helpers/acceptance_spec_helper.rb | 30 ++-- spec/helpers/compare_spec_helper.rb | 47 ++++++ spec/lib/filter/query_spec.rb | 54 +++++++ 22 files changed, 369 insertions(+), 197 deletions(-) create mode 100644 spec/helpers/compare_spec_helper.rb diff --git a/Gemfile.lock b/Gemfile.lock index 5e789222..69fe851d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -197,12 +197,12 @@ GEM railties (>= 3.0.0) fakeredis (0.5.0) redis (~> 3.0) - ffi (1.9.7) + ffi (1.9.8) formatador (0.2.5) globalid (0.3.3) activesupport (>= 4.1.0) gmaps4rails (1.5.6) - guard (2.12.4) + guard (2.12.5) formatador (>= 0.2.4) listen (~> 2.7) lumberjack (~> 1.0) diff --git a/app/controllers/audio_event_comments_controller.rb b/app/controllers/audio_event_comments_controller.rb index 71161361..f9fd505d 100644 --- a/app/controllers/audio_event_comments_controller.rb +++ b/app/controllers/audio_event_comments_controller.rb @@ -15,11 +15,11 @@ def index #@audio_event_comments = AudioEventComment.accessible_by @audio_event_comments, constructed_options = Settings.api_response.response_index( api_filter_params, - Access::Query.audio_event_comments(current_user, Access::Core.levels_allow), + get_audio_event_comments, AudioEventComment, AudioEventComment.filter_settings ) - respond_index + respond_index(constructed_options) end # GET /audio_event_comments/1 @@ -84,7 +84,7 @@ def destroy def filter filter_response = Settings.api_response.response_filter( api_filter_params, - Access::Query.audio_event_comments(current_user, Access::Core.levels_allow), + get_audio_event_comments, AudioEventComment, AudioEventComment.filter_settings ) @@ -106,4 +106,8 @@ def audio_event_comment_update_params params.permit(:format, :audio_event_id, :id, {audio_event_comment: [:flag, :comment]}) end + def get_audio_event_comments + Access::Query.audio_event_comments(current_user, Access::Core.levels_allow) + end + end diff --git a/app/controllers/bookmarks_controller.rb b/app/controllers/bookmarks_controller.rb index a75b6d90..9e509d8c 100644 --- a/app/controllers/bookmarks_controller.rb +++ b/app/controllers/bookmarks_controller.rb @@ -10,7 +10,7 @@ def index Bookmark, Bookmark.filter_settings ) - respond_index + respond_index(constructed_options) end def show diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ecc0d7b6..733f71d4 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -20,7 +20,7 @@ def index Project, Project.filter_settings ) - respond_index + respond_index(constructed_options) } end end @@ -205,7 +205,7 @@ def filter private def get_user_projects - Access::Query.projects_accessible(current_user).order('lower(name) ASC') + Access::Query.projects_accessible(current_user).order('lower(projects.name) ASC') end def project_params diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb index 83d5b8d1..4750b4aa 100644 --- a/app/controllers/sites_controller.rb +++ b/app/controllers/sites_controller.rb @@ -28,7 +28,7 @@ def index Site, Site.filter_settings ) - respond_index + respond_index(constructed_options) } end end @@ -214,13 +214,7 @@ def api_custom_response(site) end def get_user_sites - if Access::Check.is_admin?(current_user) - sites = Site.order('lower(name) ASC') - else - sites = Access::Query.sites(current_user, Access::Core.levels_allow) - end - - sites + Access::Query.sites(current_user, Access::Core.levels_allow).order('lower(sites.name) ASC') end def site_params diff --git a/app/models/audio_event.rb b/app/models/audio_event.rb index 9a50f3e6..ab662fa4 100644 --- a/app/models/audio_event.rb +++ b/app/models/audio_event.rb @@ -83,7 +83,7 @@ def self.filtered(user, params) # userId: int (optional) # audioRecordingId: int (optional) - query = Access::Query.audio_events(user, Access::Core.levels_allow) + query = Access::Query.audio_events(user, Access::Core.levels_allow).joins(:creator, :tags) query = AudioEvent.filter_reference(query, params) query = AudioEvent.filter_tags(query, params) @@ -224,7 +224,7 @@ def self.filter_paging_defaults end def self.csv_filter(user, filter_params) - query = Access::Query.audio_events(user, :reader) + query = Access::Query.audio_events(user, :reader).joins(:creator, :tags) if filter_params[:project_id] query = query.where(projects: {id: (filter_params[:project_id]).to_i}) diff --git a/config/application.rb b/config/application.rb index 83d2d597..bed4e2b6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -17,9 +17,10 @@ class Application < Rails::Application # -- all .rb files in that directory are automatically loaded. # Custom directories with classes and modules you want to be autoloadable. - config.autoload_paths += %W(#{config.root}/lib/validators) - # add all dirs recursively from lib/modules - config.autoload_paths += Dir["#{config.root}/lib/modules/**/"] + config.autoload_paths << config.root.join('lib', 'validators') + + # add /lib/modules and everything underneath it. + config.autoload_paths << config.root.join('lib', 'modules') # Custom setup # enable garbage collection profiling (reported in New Relic) diff --git a/lib/modules/access/core.rb b/lib/modules/access/core.rb index d27746c5..3156873d 100644 --- a/lib/modules/access/core.rb +++ b/lib/modules/access/core.rb @@ -247,14 +247,30 @@ def query_project_access(user, levels, query) user.id) else - # include reference audio_events when querying for audio_events only, at any level + # see http://jpospisil.com/2014/06/16/the-definitive-guide-to-arel-the-sql-manager-for-ruby.html + + projects_creator_fragment = Project.where(creator: user).select(:id) + permissions_fragment = Permission.where(user: user, level: levels).select(:project_id) + audio_event_reference_fragment = AudioEvent.where(is_reference: true).select(:id) + + pt = Project.arel_table + condition_pt = pt[:id].in(projects_creator_fragment.arel).or(pt[:id].in(permissions_fragment.arel)) + + ae = AudioEvent.arel_table + condition_ae = ae[:id].in(audio_event_reference_fragment.arel) + + # include reference audio_events when: + # - query is for audio_events or audio_event_comments model_name = query.model.model_name.name - check_reference_audio_events = (model_name == 'AudioEvent' || model_name == 'AudioEventComment') - reference_audio_events = check_reference_audio_events ? ' OR (audio_events.is_reference IS TRUE)' : '' + check_reference_audio_events = model_name == 'AudioEvent' || model_name == 'AudioEventComment' - query - .where("((projects.creator_id = ?) OR (permissions.user_id = ? AND permissions.level IN (?))#{reference_audio_events})", - user.id, user.id, levels) + if check_reference_audio_events + query.where(condition_pt.or(condition_ae)) + else + query.where(condition_pt) + end + + #query end else is_guest = Access::Check.is_guest?(user) diff --git a/lib/modules/access/query.rb b/lib/modules/access/query.rb index 814e17ec..c17b1be5 100644 --- a/lib/modules/access/query.rb +++ b/lib/modules/access/query.rb @@ -99,7 +99,7 @@ def projects(user, levels) # .joins uses inner join # need to use left outer join for permissions, as there might not be a permission, but the project # should be included because the user created it - query = Project.includes(:permissions).references(:permissions) + query = Project.all Access::Core.query_project_access(user, levels, query) end @@ -120,10 +120,7 @@ def sites(user, levels) user = Access::Core.validate_user(user) levels = Access::Core.validate_levels(levels) - query = Site - .includes(projects: [:permissions]) - .joins(:projects) - .references(:permissions) + query = Site.joins(:projects) Access::Core.query_project_access(user, levels, query) end @@ -136,10 +133,7 @@ def audio_recordings(user, levels) user = Access::Core.validate_user(user) levels = Access::Core.validate_levels(levels) - query = AudioRecording - .includes(site: [{projects: [:permissions]}]) - .joins(site: :projects) - .references(:permissions) + query = AudioRecording.joins(site: :projects) Access::Core.query_project_access(user, levels, query) end @@ -156,10 +150,7 @@ def audio_events(user, levels) # @see http://stackoverflow.com/questions/24397640/rails-nested-includes-on-active-records # Note that includes works with association names while references needs the actual table name. # @see http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-includes - query = AudioEvent - .includes([:creator, :tags, audio_recording: [{site: [{projects: [:permissions]}]}]]) - .joins(audio_recording: [site: [:projects]]) - .references(:users, :tags, :permissions) + query = AudioEvent.joins(audio_recording: [site: [:projects]]) Access::Core.query_project_access(user, levels, query) end @@ -172,10 +163,7 @@ def audio_event_comments(user, levels) user = Access::Core.validate_user(user) levels = Access::Core.validate_levels(levels) - query = AudioEventComment - .includes(audio_event: [audio_recording: [site: [{projects: [:permissions]}]]]) - .joins(audio_event: [audio_recording: [site: [:projects]]]) - .references(:permissions) + query = AudioEventComment.joins(audio_event: [audio_recording: [site: [:projects]]]) Access::Core.query_project_access(user, levels, query) end diff --git a/lib/modules/api/controller_helper.rb b/lib/modules/api/controller_helper.rb index 253849f9..01fec0bb 100644 --- a/lib/modules/api/controller_helper.rb +++ b/lib/modules/api/controller_helper.rb @@ -47,11 +47,11 @@ def set_user_id(attribute_name) end end - def respond_index + def respond_index(opts = {}) items = get_resource_plural.map { |item| respond_modify(item) } - built_response = Settings.api_response.build(:ok, items) + built_response = Settings.api_response.build(:ok, items, opts) render json: built_response, status: :ok, layout: false end diff --git a/lib/modules/api/response.rb b/lib/modules/api/response.rb index a812b83d..c26520c6 100644 --- a/lib/modules/api/response.rb +++ b/lib/modules/api/response.rb @@ -38,7 +38,6 @@ def status_phrase(status_symbol = :ok) # @option opts [Symbol] :action (nil) Action for paging links. # @option opts [Integer] :page (nil) Page number when paging. # @option opts [Integer] :items (nil) Number of items per page when paging. - # @option opts [Integer] :count (nil) Actual number of items on a page when paging. # @option opts [Integer] :total (nil) Total items matching. # @option opts [String] :filter_text (nil) Text for contains filter. # @option opts [Hash] :filter_generic_keys ({}) Property/value pairs for equality filter. @@ -48,7 +47,7 @@ def build(status_symbol = :ok, data = nil, opts = {}) { error_links: [], error_details: nil, order_by: nil, direction: nil, - page: nil, items: nil, count: nil, total: nil, + page: nil, items: nil, total: nil, filter_text: nil, filter_generic_keys: {} }) @@ -92,45 +91,23 @@ def build(status_symbol = :ok, data = nil, opts = {}) end # paging: count, total - if !opts[:count].blank? && !opts[:total].blank? + unless opts[:total].blank? result[:meta][:paging] = {} unless result[:meta].include?(:paging) - result[:meta][:paging][:count] = opts[:count] result[:meta][:paging][:total] = opts[:total] end # max page - max_page = nil if !opts[:total].blank? && !opts[:items].blank? - max_page = result[:meta][:paging][:max_page] = (opts[:total].to_f / opts[:items].to_f).ceil + max_page = (opts[:total].to_f / opts[:items].to_f).ceil + opts[:max_page] = max_page + result[:meta][:paging][:max_page] = max_page end # paging: next/prev links if result[:meta].include?(:paging) - controller = opts[:controller] - action = opts[:action] - - current_link = paging_link( - controller, action, - opts[:page], opts[:items], - opts[:filter_text], opts[:filter_generic_keys], - opts[:additional_params]) - - previous_link = paging_link( - controller, action, - restrict_to_bounds(opts[:page] - 1), - opts[:items], - opts[:filter_text], - opts[:filter_generic_keys], - opts[:additional_params] - ) - next_link = paging_link( - controller, action, - restrict_to_bounds(opts[:page] + 1, 1, max_page), - opts[:items], - opts[:filter_text], - opts[:filter_generic_keys], - opts[:additional_params] - ) + current_link = paging_link(opts, 0) + previous_link = paging_link(opts, -1) + next_link = paging_link(opts, 1) result[:meta][:paging][:current] = current_link result[:meta][:paging][:previous] = previous_link == current_link ? nil : previous_link @@ -167,12 +144,7 @@ def response_error_links(link_ids) # @param [Hash] filter_settings # @return [ActiveRecord::Relation] query def response_index(params, query, model, filter_settings) - filter_query = Filter::Query.new(params, query, model, filter_settings) - - # query without paging to get total - new_query = filter_query.query_without_filter_paging_sorting - - add_paging_and_sorting(new_query, filter_settings, filter_query) + response(params, query, model, filter_settings) end # Create and execute a query based on a filter request. @@ -183,6 +155,26 @@ def response_index(params, query, model, filter_settings) # @param [Symbol] status_symbol Response status. # @return [Hash] api response def response_filter(params, query, model, filter_settings, status_symbol = :ok) + paged_sorted_query, opts = response(params, query, model, filter_settings) + + # build response data + data = paged_sorted_query + + result = build(status_symbol, data, opts) + + # return result + result + end + + private + + # Create and execute a query based on a filter request. + # @param [Hash] params + # @param [ActiveRecord::Relation] query + # @param [ActiveRecord::Base] model + # @param [Hash] filter_settings + # @return [Array] query, options + def response(params, query, model, filter_settings) filter_query = Filter::Query.new(params, query, model, filter_settings) # query without paging to get total @@ -190,21 +182,15 @@ def response_filter(params, query, model, filter_settings, status_symbol = :ok) paged_sorted_query, opts = add_paging_and_sorting(new_query, filter_settings, filter_query) - # build response data - data = paged_sorted_query.all # build complete api response opts[:filter] = filter_query.filter unless filter_query.filter.blank? opts[:projection] = filter_query.projection unless filter_query.projection.blank? - opts[:additional_params] = params.except(model.to_s.underscore.to_sym, :filter, :projection, :action, :controller, :format) - result = build(status_symbol, data, opts) + opts[:additional_params] = params.except(model.to_s.underscore.to_sym, :filter, :projection, :action, :controller, :format, :paging, :sorting) - # return result - result + [paged_sorted_query, opts] end - private - def add_paging_and_sorting(new_query, filter_settings, filter_query) # basic options opts = { @@ -223,14 +209,10 @@ def add_paging_and_sorting(new_query, filter_settings, filter_query) # add paging new_query = filter_query.query_paging(new_query) - # execute a count for this page only - count = new_query.size - # update options opts.merge!( page: filter_query.paging[:page], items: filter_query.paging[:items], - count: count, total: total ) end @@ -273,31 +255,53 @@ def to_f_or_i_or_s(v) ((float = Float(v)) && (float % 1.0 == 0) ? float.to_i : float) rescue v end - # Create paging link for an api response. - # @param [Symbol] controller - # @param [Symbol] action - # @param [Integer] page - # @param [Integer] items - # @param [String] filter_text - # @param [Hash] filter_generic_keys - # @param [Hash] additional_params - # @return [string] paging link - def paging_link(controller, action, page = nil, items = nil, filter_text = nil, filter_generic_keys = {}, additional_params = {}) - additional_info = {} - additional_info[:controller] = controller unless controller.blank? - additional_info[:action] = action unless action.blank? - additional_info[:page] = page unless page.blank? - additional_info[:items] = items unless items.blank? - additional_info[:filter_partial_match] = filter_text unless filter_text.blank? - additional_info.merge!(additional_params) unless additional_params.blank? + # @param [Hash] opts the options for additional information. + # @option opts [Symbol] :controller (nil) Controller for paging links. + # @option opts [Symbol] :action (nil) Action for paging links. + # @option opts [Integer] :page (nil) Page number when paging. + # @option opts [Integer] :items (nil) Number of items per page when paging. + # @option opts [String] :filter_text (nil) Text for contains filter. + # @option opts [Hash] :filter_generic_keys ({}) Property/value pairs for equality filter. + # @option opts [Hash] :additional_params ({}) Additional property/value pairs. + # @param [Integer] page_offset + def paging_link(opts, page_offset) + + controller = opts[:controller] + action = opts[:action] + + page = opts[:page] + items = opts[:items] + max_page = opts[:max_page] + page = restrict_to_bounds(opts[:page] + page_offset, 1, max_page) + + disable_paging = opts[:disable_paging] + + filter_text = opts[:filter_text] + filter_generic_keys = opts[:filter_generic_keys] + additional_params = opts[:additional_params] + + order_by = opts[:order_by] + direction = opts[:direction] + + link_params = {} + link_params[:controller] = controller unless controller.blank? + link_params[:action] = action unless action.blank? + link_params[:page] = page unless page.blank? + link_params[:items] = items unless items.blank? + link_params[:disable_paging] = disable_paging unless disable_paging.blank? + link_params[:filter_partial_match] = filter_text unless filter_text.blank? + link_params[:order_by] = order_by unless order_by.blank? + link_params[:direction] = direction unless direction.blank? + link_params.merge!(additional_params) unless additional_params.blank? unless filter_generic_keys.blank? filter_generic_keys.each do |key, value| - additional_info[key] = value + link_params[('filter_' + key.to_s).to_sym] = value end end - url_helpers.url_for(additional_info) + + url_helpers.url_for(link_params) end # Get error links hash. diff --git a/lib/modules/filter/build.rb b/lib/modules/filter/build.rb index 193e78f4..b41be2e5 100644 --- a/lib/modules/filter/build.rb +++ b/lib/modules/filter/build.rb @@ -215,8 +215,8 @@ def build_condition(field, filter_name, filter_value, table, valid_fields) compose_ends_with(table, field, valid_fields, filter_value) when :not_ends_with, :not_end_with, :does_not_end_with compose_not_ends_with(table, field, valid_fields, filter_value) - #when :regex - not implemented in Arel 3. - # compose_regex(@table, field, @valid_columns, filter_value) + when :regex + compose_regex(table, field, valid_fields, filter_value) else fail CustomErrors::FilterArgumentError.new("Unrecognised filter #{filter_name}.") @@ -251,7 +251,7 @@ def build_text(text, text_fields, table, valid_fields) def build_generic(filter_hash, table, valid_fields) conditions = [] filter_hash.each do |key, value| - conditions.push(build_condition(key, :eq, value, table, valid_fields)) + conditions.push(compose_eq(table, key, valid_fields, value)) end if conditions.size > 1 diff --git a/lib/modules/filter/custom.rb b/lib/modules/filter/custom.rb index 8712f814..966d40af 100644 --- a/lib/modules/filter/custom.rb +++ b/lib/modules/filter/custom.rb @@ -66,5 +66,23 @@ def project_distance(freq_min, freq_max, annotation_duration) end end + # Compose special project ids 'in' filter. + # @param [Symbol] field + # @param [Symbol] filter_name + # @param [Object] filter_value + # @param [Arel::Table] table + # @param [Array] valid_fields + # @return [Arel::Nodes::Node] condition + def compose_sites_from_projects(field, filter_name, filter_value, table, valid_fields) + # construct special conditions + if table.name == 'sites' && field == :project_ids + # filter by many-to-many projects <-> sites + fail CustomErrors::FilterArgumentError.new("Project_ids permits only 'in' filter, got #{filter_name}.") unless filter_name == :in + projects_sites_table = Arel::Table.new(:projects_sites) + special_value = Arel::Table.new(:projects_sites).project(:site_id).where(compose_in(projects_sites_table, :project_id, [:project_id], filter_value)) + compose_in(table, :id, valid_fields, special_value) + end + end + end end \ No newline at end of file diff --git a/lib/modules/filter/parse.rb b/lib/modules/filter/parse.rb index 052103e4..7d7ec163 100644 --- a/lib/modules/filter/parse.rb +++ b/lib/modules/filter/parse.rb @@ -28,10 +28,20 @@ def parse_paging(params, default_page, default_items, max_items) items = params[:paging][:items] if items.blank? && !params[:paging].blank? disable_paging = params[:paging][:disable_paging] if disable_paging.blank? && !params[:paging].blank? + # page and items are mutually exclusive with disable_paging + fail CustomErrors::UnprocessableEntityError, 'Page and items are mutually exclusive with disable_paging' if (!page.blank? || !items.blank?) && !disable_paging.blank? + # set defaults if no setting was found page = default_page if page.blank? items = default_items if items.blank? + # ensure integer + page = page.to_i + items = items.to_i + + # ensure items is always less than max_items + fail CustomErrors::UnprocessableEntityError, "Number of items per page requested #{items} exceeded maximum #{max_items}." if items > max_items + # parse disable paging settings if disable_paging == 'true' || disable_paging == true disable_paging = true @@ -39,19 +49,9 @@ def parse_paging(params, default_page, default_items, max_items) disable_paging = false end - # ensure items is always less than max_items - fail CustomErrors::UnprocessableEntityError, "Number of items requested #{items} exceeded maximum #{max_items}." if items.to_i > max_items - - # calculate offset if able + # calculate offset and limit offset = (page - 1) * items limit = items - #page = (values.offset / values.limit) + 1 - - # ensure integer - offset = offset.to_i - limit = limit.to_i - page = page.to_i - items = items.to_i # will always set all options {offset: offset, limit: limit, page: page, items: items, disable_paging: disable_paging} @@ -69,7 +69,7 @@ def parse_sorting(params, default_order_by, default_direction) # POST body order_by = params[:sorting][:order_by] if order_by.blank? && !params[:sorting].blank? - direction = params[:sorting][:direction] if order_by.blank? && !params[:sorting].blank? + direction = params[:sorting][:direction] if direction.blank? && !params[:sorting].blank? # set defaults if necessary order_by = default_order_by if order_by.blank? diff --git a/lib/modules/filter/query.rb b/lib/modules/filter/query.rb index 8b49b452..557e573e 100644 --- a/lib/modules/filter/query.rb +++ b/lib/modules/filter/query.rb @@ -309,7 +309,7 @@ def apply_sort(query, table, column_name, allowed, direction) # @param [Integer] limit # @return [ActiveRecord::Relation] the modified query def apply_paging(query, offset, limit) - validate_paging(offset, limit, @default_items) + validate_paging(offset, limit) query.offset(offset).limit(limit) end diff --git a/lib/modules/filter/subset.rb b/lib/modules/filter/subset.rb index 979213a4..1c974809 100644 --- a/lib/modules/filter/subset.rb +++ b/lib/modules/filter/subset.rb @@ -208,16 +208,14 @@ def compose_not_range(table, column_name, allowed, from, to) end # Create regular expression condition. - # Not available just now, maybe in Arel 6? # @param [Arel::Table] table # @param [Symbol] column_name # @param [Array] allowed # @param [Object] value # @return [Arel::Nodes::Node] condition def compose_regex(table, column_name, allowed, value) - fail NotImplementedError validate_table_column(table, column_name, allowed) - table[column_name] =~ value + Arel::Nodes::Regexp.new(table[column_name], Arel::Nodes.build_quoted(value)) end # Create negated regular expression condition. @@ -228,7 +226,8 @@ def compose_regex(table, column_name, allowed, value) # @param [Object] value # @return [Arel::Nodes::Node] condition def compose_not_regex(table, column_name, allowed, value) - compose_regex(table, column_name, allowed, value).not + validate_table_column(table, column_name, allowed) + Arel::Nodes::NotRegexp.new(table[column_name], Arel::Nodes.build_quoted(value)) end end diff --git a/lib/modules/filter/validate.rb b/lib/modules/filter/validate.rb index 619e129d..f191633d 100644 --- a/lib/modules/filter/validate.rb +++ b/lib/modules/filter/validate.rb @@ -37,47 +37,23 @@ def validate_sorting(order_by, valid_fields, direction) # Validate paging values. # @param [Integer] offset # @param [Integer] limit - # @param [Integer] max_limit # @return [void] - def validate_paging(offset, limit, max_limit) - if !offset.blank? && !limit.blank? - # allow both to be nil, but if one is nil and the other is not, that is an error. - fail CustomErrors::FilterArgumentError, "Offset must be an integer, got #{offset}" if offset.blank? || offset != offset.to_i - fail CustomErrors::FilterArgumentError, "Limit must be an integer, got #{limit}" if limit.blank? || limit != limit.to_i - fail CustomErrors::FilterArgumentError, "Max must be an integer, got #{max_limit}" if max_limit.blank? || max_limit != max_limit.to_i - - offset_i = offset.to_i - limit_i = limit.to_i - max_limit_i = max_limit.to_i - - fail CustomErrors::FilterArgumentError, "Offset must be 0 or greater, got #{offset_i}" if offset_i < 0 - fail CustomErrors::FilterArgumentError, "Limit must be greater than 0, got #{limit_i}" if limit_i < 1 - fail CustomErrors::FilterArgumentError, "Max must be greater than 0, got #{max_limit_i}" if max_limit_i < 1 - end + def validate_paging(offset, limit) + validate_integer(offset, 0) + validate_integer(limit, 1) end - # Validate paging values. - # @param [Integer] page - # @param [Integer] items - # @param [Integer] max_items - # @return [void] - def validate_paging_external(page, items, max_items) - if !page.blank? && !items.blank? - # allow both to be nil, but if one is nil and the other is not, that is an error. - fail CustomErrors::FilterArgumentError, "Page must be an integer, got #{page}" if page.blank? || page != page.to_i - fail CustomErrors::FilterArgumentError, "Items must be an integer, got #{items}" if items.blank? || items != items.to_i - fail CustomErrors::FilterArgumentError, "Max must be an integer, got #{max_items}" if max_items.blank? || max_items != max_items.to_i + def validate_integer(value, min = nil, max = nil) + fail CustomErrors::FilterArgumentError, 'Value must not be blank' if value.blank? + fail CustomErrors::FilterArgumentError, "Value must be an integer, got #{value}" if value.blank? || value != value.to_i - page_i = page.to_i - items_i = items.to_i - max_items_i = max_items.to_i + value_i = value.to_i - fail CustomErrors::FilterArgumentError, "Page must be greater than 0, got #{page_i}" if page_i < 1 - fail CustomErrors::FilterArgumentError, "Items must be greater than 0, got #{items_i}" if items_i < 1 - fail CustomErrors::FilterArgumentError, "Max must be greater than 0, got #{max_items_i}" if max_items_i < 1 - end + fail CustomErrors::FilterArgumentError, "Value must be #{min} or greater, got #{value_i}" if !min.blank? && value_i < min + fail CustomErrors::FilterArgumentError, "Value must be #{max} or less, got #{value_i}" if !max.blank? && value_i > max end + # Validate query, table, and column values. # @param [Arel::Query] query # @param [Arel::Table] table @@ -160,7 +136,7 @@ def validate_column_name(column_name, allowed) # @raise [FilterArgumentError] if model is not an ActiveRecord::Base # @return [void] def validate_model(model) - fail CustomErrors::FilterArgumentError, "Model must respond to all, got #{model.class}" unless model.respond_to?(:all) + fail CustomErrors::FilterArgumentError, "Model must be an ActiveRecord::Base, got #{model.base_class}" unless model < ActiveRecord::Base end # Validate an array. @@ -169,7 +145,7 @@ def validate_model(model) # @return [void] def validate_array(value) fail CustomErrors::FilterArgumentError, "Value must not be null, got #{value}" if value.blank? - fail CustomErrors::FilterArgumentError, "Value must be an Array or Arel::SelectManager, got #{value}" unless value.is_a?(Array) || value.is_a?(Arel::SelectManager) + fail CustomErrors::FilterArgumentError, "Value must be an Array or Arel::SelectManager, got #{value.class}" unless value.is_a?(Array) || value.is_a?(Arel::SelectManager) end # Validate array items. Do not validate if value is not an Array. diff --git a/spec/acceptance/audio_recordings_spec.rb b/spec/acceptance/audio_recordings_spec.rb index 78c061b4..6feb83bb 100644 --- a/spec/acceptance/audio_recordings_spec.rb +++ b/spec/acceptance/audio_recordings_spec.rb @@ -291,7 +291,7 @@ def test_overlap # ensure posted audio recording does not exist expect(AudioRecording.where(file_hash: posted_item_attrs[:file_hash]).count) - .to eq(0), "all file_hashes #{AudioRecording.select(:file_hash).all}, input posted #{posted_item_attrs[:file_hash]}" + .to eq(0), "all file_hashes #{AudioRecording.select(:file_hash).all}, input posted #{posted_item_attrs[:file_hash]}" else raise "unknown status code #{status_code}" end @@ -846,7 +846,10 @@ def test_overlap } }.to_json } let(:authentication_token) { reader_token } - standard_request_options(:post, 'FILTER (as reader matching)', :ok, {expected_json_path: 'data/0/sample_rate_hertz', data_item_count: 1}) + standard_request_options(:post, 'FILTER (as reader matching)', :ok, { + expected_json_path: 'data/0/sample_rate_hertz', + data_item_count: 1 + }) end post '/audio_recordings/filter' do @@ -864,7 +867,10 @@ def test_overlap } }.to_json } let(:authentication_token) { reader_token } - standard_request_options(:post, 'FILTER (as reader no match)', :ok, {expected_json_path: 'meta/message', data_item_count: 0}) + standard_request_options(:post, 'FILTER (as reader no match)', :ok, { + expected_json_path: 'meta/message', + data_item_count: 0 + }) end post '/audio_recordings/filter' do @@ -885,7 +891,60 @@ def test_overlap } }.to_json } let(:authentication_token) { reader_token } - standard_request_options(:post, 'FILTER (as reader with paging)', :ok, {expected_json_path: 'meta/paging/page', data_item_count: 0}) + standard_request_options(:post, 'FILTER (as reader with paging)', :ok, { + expected_json_path: 'meta/paging/page', + data_item_count: 0, + response_body_content: '/audio_recordings/filter?direction=desc\u0026items=30\u0026order_by=recorded_date\u0026page=1' + }) + end + + post '/audio_recordings/filter' do + let(:raw_post) { { + filter: { + and: { + site_id: { + less_than: 123456 + }, + duration_seconds: { + not_eq: 40 + } + } + }, + sorting: { + order_by: :channels, + direction: :asc + } + }.to_json } + let(:authentication_token) { reader_token } + standard_request_options(:post, 'FILTER (as reader with sorting)', :ok, { + expected_json_path: 'meta/sorting/direction', + data_item_count: 1, + response_body_content: '/audio_recordings/filter?direction=asc\u0026items=25\u0026order_by=channels\u0026page=1' + }) + end + + post '/audio_recordings/filter' do + let(:raw_post) { { + filter: { + and: { + site_id: { + less_than: 123456 + }, + duration_seconds: { + not_eq: 40 + } + } + }, + projection: { + include: [:id, :site_id, :duration_seconds, :recorded_date, :created_at] + } + }.to_json } + let(:authentication_token) { reader_token } + standard_request_options(:post, 'FILTER (as reader with projection)', :ok, { + expected_json_path: 'meta/projection/include', + data_item_count: 1, + response_body_content: '/audio_recordings/filter?direction=desc\u0026items=25\u0026order_by=recorded_date\u0026page=1' + }) end post '/audio_recordings/filter' do @@ -899,11 +958,11 @@ def test_overlap {"orderBy" => "createdAt", "direction" => "desc"}} .to_json } let(:authentication_token) { reader_token } - standard_request_options(:post, 'FILTER (as reader with paging, sorting, projection)', :ok, { - expected_json_path: 'meta/paging/current', + standard_request_options(:post, 'FILTER (as reader checking camel case)', :ok, { + expected_json_path: 'meta/projection/include', data_item_count: 1, - invalid_content: ['"status":', '"notes"'], - response_body_content: 'http://localhost:3000/audio_recordings/filter?items=10\u0026page=1\u0026direction=desc\u0026orderBy=createdAt' + invalid_data_content: (AudioRecording.filter_settings[:render_fields] - [:id, :site_id, :duration_seconds, :recorded_date, :created_at]).map { |i| "\"#{i.to_s}\":" }, + response_body_content: '/audio_recordings/filter?direction=desc\u0026items=10\u0026order_by=created_at\u0026page=1' }) end diff --git a/spec/acceptance/projects_spec.rb b/spec/acceptance/projects_spec.rb index eb23f558..b4fb9de1 100644 --- a/spec/acceptance/projects_spec.rb +++ b/spec/acceptance/projects_spec.rb @@ -22,6 +22,7 @@ # Create post parameters from factory let(:post_attributes) { FactoryGirl.attributes_for(:project) } + let(:project_name) { @write_permission.project.name } before(:each) do # this creates a @write_permission.user with write access to @write_permission.project, @@ -36,7 +37,10 @@ ################################ get '/projects' do let(:authentication_token) { writer_token } - standard_request_options(:get, 'LIST (as confirmed_user)', :ok, {expected_json_path: 'data/0/name', data_item_count: 1}) + standard_request_options(:get, 'LIST (as confirmed_user)', :ok, { + expected_json_path: 'data/0/name', + data_item_count: 1 + }) end get '/projects' do @@ -205,7 +209,19 @@ }.to_json } let(:authentication_token) { reader_token } - standard_request_options(:post, 'FILTER (as reader)', :ok, {expected_json_path: 'data/0/name', data_item_count: 1}) + standard_request_options(:post, 'FILTER (as reader)', :ok, { + expected_json_path: ['data/0/name', 'meta/projection/include'], + data_item_count: 1 + }) + end + + get '/projects/filter?direction=desc&filter_name=a&filter_partial_match=partial_match_text&items=35&order_by=createdAt&page=1' do + let(:authentication_token) { reader_token } + standard_request_options(:get, 'BASIC FILTER (as reader with filtering, sorting, paging)', :ok, { + expected_json_path: 'meta/paging/current', + data_item_count: 0, + response_body_content: '/projects/filter?direction=desc\u0026filter_name=a\u0026filter_partial_match=partial_match_text\u0026items=35\u0026order_by=createdAt\u0026page=1' + }) end end \ No newline at end of file diff --git a/spec/helpers/acceptance_spec_helper.rb b/spec/helpers/acceptance_spec_helper.rb index c6780a16..22bee343 100644 --- a/spec/helpers/acceptance_spec_helper.rb +++ b/spec/helpers/acceptance_spec_helper.rb @@ -1,3 +1,5 @@ +require 'helpers/compare_spec_helper' + def document_media_requests # this is here so rspec_api_documentation can be generated # any request that returns content that cannot be json serialised (e.g. binary data) @@ -45,9 +47,10 @@ def standard_request(description, expected_status, expected_json_path = nil, doc # @param [Hash] opts the options for additional information. # @option opts [String] :expected_json_path (nil) Expected json path. # @option opts [Boolean] :document (true) Include in api spec documentation. -# @option opts [Symbol] :response_body_content (nil) Content that must be in the response body. -# @option opts [Symbol] :invalid_content (nil) Content that must not be in the response body. -# @option opts [Symbol] :data_item_count (nil) Number of items in a json response +# @option opts [String,Array] :response_body_content (nil) Content that must be in the response body. +# @option opts [String,Array] :invalid_content (nil) Content that must not be in the response body. +# @option opts [String,Array] :invalid_data_content (nil) Content that must not be in the response data. +# @option opts [Integer] :data_item_count (nil) Number of items in a json response # @option opts [Hash] :property_match (nil) Properties to match # @option opts [Hash] :file_exists (nil) Check if file exists # @option opts [Class] :expected_error_class (nil) The expected error class @@ -242,8 +245,9 @@ def acceptance_checks_shared(request, opts = {}) # Check json response. # @param [Hash] opts the options for additional information. # @option opts [String] :expected_json_path (nil) Expected json path. -# @option opts [Symbol] :response_body_content (nil) Content that must be in the response body. -# @option opts [Symbol] :invalid_content (nil) Content that must not be in the response body. +# @option opts [String,Array] :response_body_content (nil) Content that must be in the response body. +# @option opts [String,Array] :invalid_content (nil) Content that must not be in the response body. +# @option opts [String,Array] :invalid_data_content (nil) Content that must not be in the response data. # @option opts [Symbol] :data_item_count (nil) Number of items in a json response # @option opts [Hash] :property_match (nil) Properties to match # @option opts [Regex] :regex_match (nil) Regex that must match content @@ -254,6 +258,7 @@ def acceptance_checks_json(opts = {}) expected_json_path: nil, response_body_content: nil, invalid_content: nil, + invalid_data_content: nil, data_item_count: nil, property_match: nil, regex_match: nil @@ -291,16 +296,9 @@ def acceptance_checks_json(opts = {}) expect(opts[:actual_response]).to include(opts[:response_body_content]), "#{message_prefix} to find '#{opts[:response_body_content]}' in '#{opts[:actual_response]}'" unless opts[:response_body_content].blank? - unless opts[:invalid_content].blank? - if opts[:invalid_content].respond_to?(:each) - opts[:invalid_content].each do |invalid_content_item| - expect(opts[:actual_response]).to_not include(invalid_content_item), "#{message_prefix} not to find '#{invalid_content_item}' in '#{opts[:actual_response]}'" - end - else - expect(opts[:actual_response]).to_not include(opts[:invalid_content]), "#{message_prefix} not to find '#{opts[:invalid_content]}' in '#{opts[:actual_response]}'" - end + check_invalid_content(opts, message_prefix) - end + check_invalid_data_content(opts, message_prefix, actual_response_parsed) unless opts[:expected_json_path].blank? @@ -336,9 +334,7 @@ def acceptance_checks_json(opts = {}) end end - unless opts[:regex_match].nil? - expect(opts[:actual_response]).to match(opts[:regex_match]) - end + check_regex_match(opts) end diff --git a/spec/helpers/compare_spec_helper.rb b/spec/helpers/compare_spec_helper.rb new file mode 100644 index 00000000..8616ef9a --- /dev/null +++ b/spec/helpers/compare_spec_helper.rb @@ -0,0 +1,47 @@ +# expect(actual).to be(expected) + +def check_regex_match(opts) + + actual = opts[:actual_response] + expected = opts[:regex_match] + + if opts.has_key?(:regex_match) && !expected.blank? + fail ArgumentError, 'Must include :actual_response to check :regex_match' unless opts.has_key?(:actual_response) + expect(actual).to match(expected) + end + +end + +def check_invalid_content(opts, message_prefix) + + actual = opts[:actual_response] + expected = opts[:invalid_content] + + unless expected.blank? + if expected.respond_to?(:each) + expected.each do |invalid_content_item| + expect(actual).to_not include(invalid_content_item), "#{message_prefix} not to find '#{invalid_content_item}' in '#{actual}'" + end + else + expect(actual).to_not include(expected), "#{message_prefix} not to find '#{expected}' in '#{actual}'" + end + + end +end + +def check_invalid_data_content(opts, message_prefix, parsed_response) + + expected = opts[:invalid_data_content] + + if !expected.blank? && !parsed_response.blank? + actual = parsed_response['data'].to_json + if expected.respond_to?(:each) + expected.each do |invalid_content_item| + expect(actual).to_not include(invalid_content_item), "#{message_prefix} not to find '#{invalid_content_item}' in '#{actual}'" + end + else + expect(actual).to_not include(expected), "#{message_prefix} not to find '#{expected}' in '#{actual}'" + end + + end +end \ No newline at end of file diff --git a/spec/lib/filter/query_spec.rb b/spec/lib/filter/query_spec.rb index a4782b2c..c888b457 100644 --- a/spec/lib/filter/query_spec.rb +++ b/spec/lib/filter/query_spec.rb @@ -515,6 +515,60 @@ def create_filter(params) ORDERBY\"audio_recordings\".\"duration_seconds\"DESCLIMIT10OFFSET0" compare_filter_sql(complex_sample, complex_result) + + @permission = FactoryGirl.create(:write_permission) + user = @permission.user + user_id = user.id + + complex_result_2 = + "SELECT\"audio_recordings\".\"recorded_date\",\"audio_recordings\".\"site_id\", \ +\"audio_recordings\".\"duration_seconds\",\"audio_recordings\".\"media_type\" \ +FROM\"audio_recordings\" \ +INNERJOIN\"sites\"ON\"sites\".\"id\"=\"audio_recordings\".\"site_id\"AND(\"sites\".\"deleted_at\"ISNULL) \ +INNERJOIN\"projects_sites\"ON\"projects_sites\".\"site_id\"=\"sites\".\"id\" \ +INNERJOIN\"projects\"ON\"projects\".\"id\"=\"projects_sites\".\"project_id\"AND(\"projects\".\"deleted_at\"ISNULL) \ +WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ +AND(\"projects\".\"id\"IN(SELECT\"projects\".\"id\"FROM\"projects\"WHERE(\"projects\".\"deleted_at\"ISNULL)AND\"projects\".\"creator_id\"=#{user_id}) \ +OR\"projects\".\"id\"IN(SELECT\"permissions\".\"project_id\"FROM\"permissions\"WHERE\"permissions\".\"user_id\"=#{user_id}AND\"permissions\".\"level\"IN('reader','writer','owner'))) \ +AND(\"audio_recordings\".\"site_id\"<123456 \ +AND\"audio_recordings\".\"site_id\">9876 \ +AND\"audio_recordings\".\"site_id\"IN(1,2,3) \ +AND\"audio_recordings\".\"site_id\">=100 \ +AND\"audio_recordings\".\"site_id\"<200 \ +AND\"audio_recordings\".\"status\">='4567' \ +AND\"audio_recordings\".\"status\"ILIKE'%containtext%' \ +AND\"audio_recordings\".\"status\"ILIKE'startswithtext%' \ +AND\"audio_recordings\".\"status\"ILIKE'%endswithtext' \ +AND\"audio_recordings\".\"status\">='123' \ +AND\"audio_recordings\".\"status\"<='128' \ +AND(\"audio_recordings\".\"duration_seconds\"!=40 \ +ORNOT(\"audio_recordings\".\"channels\"<=9999))) \ +AND(((((((\"audio_recordings\".\"recorded_date\"ILIKE'%Hello%' \ +OR\"audio_recordings\".\"media_type\"ILIKE'%world') \ +OR\"audio_recordings\".\"duration_seconds\"=60) \ +OR\"audio_recordings\".\"duration_seconds\"<=70) \ +OR\"audio_recordings\".\"duration_seconds\"=50) \ +OR\"audio_recordings\".\"duration_seconds\">=80) \ +OR\"audio_recordings\".\"channels\"=1) \ +OR\"audio_recordings\".\"channels\"<=8888) \ +AND(NOT(\"audio_recordings\".\"duration_seconds\"!=140)) \ +AND(\"audio_recordings\".\"media_type\"ILIKE'%testing\\_testing%' \ +OR\"audio_recordings\".\"status\"ILIKE'%testing\\_testing%') \ +AND(\"audio_recordings\".\"status\"='hello' \ +AND\"audio_recordings\".\"channels\"=28) \ +ORDERBY\"audio_recordings\".\"duration_seconds\"DESCLIMIT10OFFSET0" + + + + filter_query = Filter::Query.new( + complex_sample, + Access::Query.audio_recordings(user, Access::Core.levels_allow), + AudioRecording, + AudioRecording.filter_settings + ) + + expect(filter_query.query_full.to_sql.gsub(/\s+/, '')).to eq(complex_result_2.gsub(/\s+/, '')) + end end end From c17d0fa69c16bc8b96d8ea9bff53826f0ed1d8ae Mon Sep 17 00:00:00 2001 From: cofiem Date: Tue, 17 Mar 2015 00:22:50 +1000 Subject: [PATCH 37/49] Added assocations spec filter_settings in 3 models for #176. Not complete yet, but can add condition on other tables to query. Still to add `distinct` and joins to required tables. --- app/models/audio_event.rb | 27 +++- app/models/audio_recording.rb | 34 ++++- app/models/tag.rb | 40 ++++++ config/routes.rb | 9 +- lib/modules/filter/build.rb | 130 +++++++++++++++--- lib/modules/filter/query.rb | 6 +- lib/modules/filter/validate.rb | 31 +++++ spec/lib/filter/query_spec.rb | 44 +++++- .../audio_event_comments_routing_spec.rb | 5 +- spec/routing/audio_events_routing_spec.rb | 14 +- spec/routing/audio_recordings_routing_spec.rb | 22 ++- spec/routing/bookmarks_routing_spec.rb | 5 +- spec/routing/devise_routing_spec.rb | 6 +- spec/routing/media_routing_spec.rb | 2 +- spec/routing/projects_routing_spec.rb | 8 +- spec/routing/sites_routing_spec.rb | 5 +- spec/routing/taggings_routing_spec.rb | 7 +- spec/routing/tags_routing_spec.rb | 8 +- spec/routing/user_accounts_routing_spec.rb | 1 + 19 files changed, 343 insertions(+), 61 deletions(-) diff --git a/app/models/audio_event.rb b/app/models/audio_event.rb index ab662fa4..bcfe0a2c 100644 --- a/app/models/audio_event.rb +++ b/app/models/audio_event.rb @@ -64,7 +64,32 @@ def self.filter_settings defaults: { order_by: :created_at, direction: :desc - } + }, + valid_associations: [ + { + join: AudioRecording, + on: AudioEvent.arel_table[:audio_recording_id].eq(AudioRecording.arel_table[:id]), + available: true + }, + { + join: AudioEventComment, + on: AudioEvent.arel_table[:id].eq(AudioEventComment.arel_table[:audio_event_id]), + available: true + }, + { + join: Tagging, + on: AudioEvent.arel_table[:id].eq(Tagging.arel_table[:audio_event_id]), + available: false, + associations: [ + { + join: Tag, + on: Tagging.arel_table[:tag_id].eq(Tag.arel_table[:id]), + available: true + } + ] + + } + ] } end diff --git a/app/models/audio_recording.rb b/app/models/audio_recording.rb index 1d9b2b67..16ac501e 100644 --- a/app/models/audio_recording.rb +++ b/app/models/audio_recording.rb @@ -214,7 +214,39 @@ def self.filter_settings defaults: { order_by: :recorded_date, direction: :desc - } + }, + valid_associations: [ + { + join: AudioEvent, + on: AudioRecording.arel_table[:id].eq(AudioEvent.arel_table[:audio_recording_id]), + available: true, + associations: [ + { + join: Tagging, + on: AudioEvent.arel_table[:id].eq(Tagging.arel_table[:audio_event_id]), + available: false, + associations: [ + { + join: Tag, + on: Tagging.arel_table[:tag_id].eq(Tag.arel_table[:id]), + available: true + } + ] + + } + ] + }, + { + join: Site, + on: AudioRecording.arel_table[:site_id].eq(Site.arel_table[:id]), + available: true + }, + { + join: Bookmark, + on: AudioRecording.arel_table[:id].eq(Bookmark.arel_table[:audio_recording_id]), + available: true + } + ] } end diff --git a/app/models/tag.rb b/app/models/tag.rb index 790e1900..954ef9b8 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -74,4 +74,44 @@ def self.get_priority_tag(tags) first_common || first_species || first_other end + # Define filter api settings + def self.filter_settings + { + valid_fields: [ + :id, :text, :is_taxanomic, :type_of_tag, :retired, :notes, + :creator_id, :created_at, :updater_id, :updated_at + ], + render_fields: [:id, :text, :is_taxanomic, :type_of_tag, :retired], + text_fields: [:text, :type_of_tag, :notes], + controller: :tags, + action: :filter, + defaults: { + order_by: :text, + direction: :asc + }, + valid_associations: [ + { + join: Tagging, + on: Tag.arel_table[:id].eq(Tagging.arel_table[:tag_id]), + available: false, + associations: [ + { + join: AudioEvent, + on: Tagging.arel_table[:id].eq(AudioEvent.arel_table[:audio_event_id]), + available: true, + associations: [ + { + join: AudioRecording, + on: AudioEvent.arel_table[:audio_recording_id].eq(AudioRecording.arel_table[:id]), + available: true, + } + ] + } + ] + + } + ] + } + end + end diff --git a/config/routes.rb b/config/routes.rb index 9ca05731..5b29b221 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -118,10 +118,11 @@ # audio_recording_update: /audio_recordings/:id # routes used by harvester: - # endpoint_login: /security/sign_in - # endpoint_create: /projects/:project_id/sites/:site_id/audio_recordings - # endpoint_check_uploader: /projects/:project_id/sites/:site_id/audio_recordings/check_uploader/:uploader_id - # endpoint_update_status: /audio_recordings/:id/update_status + # login: /security + # audio_recording: /audio_recordings/:id + # audio_recording_create: /projects/:project_id/sites/:site_id/audio_recordings + # audio_recording_uploader: /projects/:project_id/sites/:site_id/audio_recordings/check_uploader/:uploader_id + # audio_recording_update_status: /audio_recordings/:id/update_status # endpoints used by client: # routes: { diff --git a/lib/modules/filter/build.rb b/lib/modules/filter/build.rb index b41be2e5..3483ebd5 100644 --- a/lib/modules/filter/build.rb +++ b/lib/modules/filter/build.rb @@ -17,15 +17,15 @@ module Build # Build conditions from a hash. # @param [Hash] hash # @param [Arel::Table] table - # @param [Array] valid_fields + # @param [Hash] filter_settings # @return [Array] conditions - def build_top(hash, table, valid_fields) + def build_top(hash, table, filter_settings) fail CustomErrors::FilterArgumentError.new("Conditions hash must have at least 1 entry, got #{hash.size}.", {hash: hash}) if hash.blank? || hash.size < 1 conditions = [] hash.each do |key, value| # combinators or fields can be at top level. Assumes 'and' (query.where(condition) uses 'and'). - built_conditions = build_hash(key, value, table, valid_fields) + built_conditions = build_hash(key, value, table, filter_settings) conditions.push(*built_conditions) end conditions @@ -35,21 +35,21 @@ def build_top(hash, table, valid_fields) # @param [Symbol] field # @param [Hash] hash # @param [Arel::Table] table - # @param [Array] valid_fields + # @param [Hash] filter_settings # @return [Array] conditions - def build_hash(field, hash, table, valid_fields) + def build_hash(field, hash, table, filter_settings) fail CustomErrors::FilterArgumentError.new("Conditions hash must have at least 1 entry, got #{hash.size}.", {field: field, hash: hash}) if hash.blank? || hash.size < 1 fail CustomErrors::FilterArgumentError.new("'Not' must have a single combiner or field name, got #{hash.size}", {field: field, hash: hash}) if field == :not && hash.size != 1 conditions = [] case field when :and, :or - conditions_to_combine = build_hashes(hash, table, valid_fields) + conditions_to_combine = build_hashes(hash, table, filter_settings) combined_conditions = build_combiner(field, conditions_to_combine) conditions.push(combined_conditions) else hash.each do |key, value| - conditions.push(build_field(field, key, value, table, valid_fields)) + conditions.push(build_field(field, key, value, table, filter_settings)) end end @@ -59,12 +59,12 @@ def build_hash(field, hash, table, valid_fields) # Build conditions from nested hashes. # @param [Hash] hash # @param [Arel::Table] table - # @param [Array] valid_fields + # @param [Hash] filter_settings # @return [Array] conditions - def build_hashes(hash, table, valid_fields) + def build_hashes(hash, table, filter_settings) conditions = [] hash.each do |key, value| - built_conditions = build_hash(key, value, table, valid_fields) + built_conditions = build_hash(key, value, table, filter_settings) conditions.push(*built_conditions) end conditions @@ -75,14 +75,18 @@ def build_hashes(hash, table, valid_fields) # @param [Symbol] key # @param [Object] value # @param [Arel::Table] table - # @param [Array] valid_fields + # @param [Hash] filter_settings # @return [Arel::Nodes::Node] condition - def build_field(field, key, value, table, valid_fields) + def build_field(field, key, value, table, filter_settings) + valid_fields = filter_settings[:valid_fields].map(&:to_sym) case field when :not - build_not(key, value, table, valid_fields) + build_not(key, value, table, filter_settings) when *valid_fields - build_condition(field, key, value, table, valid_fields) + build_condition(field, key, value, table, filter_settings) + when /\./ + table_mod, field_mod, filter_settings_mod = build_table_field(table, field, filter_settings) + build_condition(field_mod, key, value, table_mod, filter_settings_mod) else fail CustomErrors::FilterArgumentError.new("Unrecognised combiner or field name: #{field}.") end @@ -93,12 +97,12 @@ def build_field(field, key, value, table, valid_fields) # @param [Symbol] key # @param [Object] value # @param [Arel::Table] table - # @param [Array] valid_fields + # @param [Hash] filter_settings # @return [Arel::Nodes::Node] condition - def build_multiple(field, key, value, table, valid_fields) + def build_multiple(field, key, value, table, filter_settings) case field when :and, :or - build_combiner(field, build_hash(key, value, table, valid_fields)) + build_combiner(field, build_hash(key, value, table, filter_settings)) else fail CustomErrors::FilterArgumentError.new("Unrecognised combiner or field name: #{field}", {field: key, hash: value}) end @@ -145,14 +149,15 @@ def build_combiner(combiner, conditions) # @param [Symbol] field # @param [Hash] hash # @param [Arel::Table] table - # @param [Array] valid_fields + # @param [Hash] filter_settings # @return [Arel::Nodes::Node] condition - def build_not(field, hash, table, valid_fields) + def build_not(field, hash, table, filter_settings) fail CustomErrors::FilterArgumentError.new("'Not' must have a single filter, got #{hash.size}.", {field: field, hash: hash}) if hash.size != 1 negated_condition = nil hash.each do |key, value| - condition = build_condition(field, key, value, table, valid_fields) + table_mod, field_mod, filter_settings_mod = build_table_field(table, field, filter_settings) + condition = build_condition(field_mod, key, value, table_mod, filter_settings_mod) negated_condition = compose_not(condition) end @@ -164,9 +169,10 @@ def build_not(field, hash, table, valid_fields) # @param [Symbol] filter_name # @param [Object] filter_value # @param [Arel::Table] table - # @param [Array] valid_fields + # @param [Hash] filter_settings # @return [Arel::Nodes::Node] condition - def build_condition(field, filter_name, filter_value, table, valid_fields) + def build_condition(field, filter_name, filter_value, table, filter_settings) + valid_fields = filter_settings[:valid_fields].map(&:to_sym) special_condition = build_condition_special(field, filter_name, filter_value, table, valid_fields) return special_condition unless special_condition.nil? @@ -322,5 +328,85 @@ def build_condition_special(field, filter_name, filter_value, table, valid_field end end + def build_field_info(table_name, field_name) + model = table_name.to_s.classify.constantize + model_filter_settings = model.filter_settings + model_valid_fields = model_filter_settings[:valid_fields].map(&:to_sym) + field_sym = field_name.to_sym + arel_table = relation_table(model) + + validate_table_column(arel_table, field_sym, model_valid_fields) + + { + table_name: table_name, + field_name: field_sym, + arel_table: arel_table, + model: model, + filter_settings: model_filter_settings + } + end + + # Build table field from field symbol. + # @param [Arel::Table] table + # @param [Symbol] field + # @param [Hash] filter_settings + # @return [Arel::Table, Symbol, Hash] table, field, filter_settings + def build_table_field(table, field, filter_settings) + validate_table(table) + fail CustomErrors::FilterArgumentError, 'Field name must be a symbol.' unless field.is_a?(Symbol) + + field_s = field.to_s + + if field_s.include?('.') + dot_index = field.to_s.index('.') + parsed_table = field[0, dot_index] + parsed_field = field[(dot_index + 1)..field.length] + + info = build_field_info(parsed_table, parsed_field) + + associations = build_associations(filter_settings[:valid_associations], table) + models = associations.map { |a| a[:join] } + + validate_association(info[:model], models) + + [info[:arel_table], info[:field_name], info[:filter_settings]] + else + [table, field, filter_settings] + end + + end + + # Parse association_allowed hashes and arrays to get names. + # @param [Hash, Array] valid_associations + # @param [Arel::Table] table + # @return [Arel::Table, Symbol, Hash] table, field, filter_settings + def build_associations(valid_associations, table) + + associations = [] + if valid_associations.is_a?(Array) + more_associations = valid_associations.map { |i| build_associations(i, table) } + associations.push(*more_associations.flatten.compact) if more_associations.size > 0 + elsif valid_associations.is_a?(Hash) + + join = valid_associations[:join] + on = valid_associations[:on] + available = valid_associations[:available] + + more_associations = build_associations(valid_associations[:associations], join) + associations.push(*more_associations.flatten.compact) if more_associations.size > 0 + + if available + associations.push( + { + join: join, + on: on + }) + end + + end + + associations + end + end end \ No newline at end of file diff --git a/lib/modules/filter/query.rb b/lib/modules/filter/query.rb index 557e573e..53f456f8 100644 --- a/lib/modules/filter/query.rb +++ b/lib/modules/filter/query.rb @@ -133,7 +133,7 @@ def query_without_filter_paging_sorting # @return [ActiveRecord::Relation] query def query_filter(query) if has_filter_params? - apply_conditions(query, build_top(@filter, @table, @valid_fields)) + apply_conditions(query, build_top(@filter, @table, @filter_settings)) else query end @@ -170,6 +170,7 @@ def query_projection_default(query) # @return [ActiveRecord::Relation] query def query_filter_text(query) return query unless has_qsp_text? + # only text fields on the /filter model can be used - can't filter on other table fields text_condition = build_text(@qsp_text_filter, @text_fields, @table, @valid_fields) apply_condition(query, text_condition) end @@ -179,6 +180,7 @@ def query_filter_text(query) # @param [String] filter_text # @return [ActiveRecord::Relation] query def query_filter_text_custom(query, filter_text) + # only text fields on the /filter model can be used - can't filter on other table fields text_condition = build_text(filter_text, @text_fields, @table, @valid_fields) apply_condition(query, text_condition) end @@ -188,6 +190,7 @@ def query_filter_text_custom(query, filter_text) # @return [ActiveRecord::Relation] query def query_filter_generic(query) return query unless has_qsp_generic? + # only fields on the /filter model can be used - can't filter on other table fields apply_condition(query, build_generic(@qsp_generic_filters, @table, @valid_fields)) end @@ -196,6 +199,7 @@ def query_filter_generic(query) # @param [Hash] filter_hash # @return [ActiveRecord::Relation] query def query_filter_generic_custom(query, filter_hash) + # only fields on the /filter model can be used - can't filter on other table fields apply_condition(query, build_generic(filter_hash, @table, @valid_fields)) end diff --git a/lib/modules/filter/validate.rb b/lib/modules/filter/validate.rb index f191633d..38476346 100644 --- a/lib/modules/filter/validate.rb +++ b/lib/modules/filter/validate.rb @@ -76,6 +76,13 @@ def validate_table_column(table, column_name, allowed) validate_column_name(column_name, allowed) end + def validate_association(model, models_allowed) + validate_model(model) + + fail CustomErrors::FilterArgumentError, "Models allowed must be an Array, got #{models_allowed}" unless models_allowed.is_a?(Array) + fail CustomErrors::FilterArgumentError, "Model must be in #{models_allowed}, got #{model}" unless models_allowed.include?(model) + end + # Validate query and hash values. # @param [ActiveRecord::Relation] query # @param [Hash] hash @@ -245,5 +252,29 @@ def validate_float(value) end + def validate_filter_settings(value) + validate_hash(value) + + validate_array(value[:valid_fields]) + validate_array_items(value[:valid_fields]) + + validate_array(value[:render_fields]) + validate_array_items(value[:render_fields]) + + validate_array(value[:text_fields]) + validate_array_items(value[:text_fields]) + + fail CustomErrors::FilterArgumentError, 'Controller name must be a symbol.' unless value[:controller].is_a?(Symbol) + fail CustomErrors::FilterArgumentError, 'Action name must be a symbol.' unless value[:action].is_a?(Symbol) + + validate_hash(value[:defaults]) + + fail CustomErrors::FilterArgumentError, 'Order by must be a symbol.' unless value[:defaults][:order_by].is_a?(Symbol) + fail CustomErrors::FilterArgumentError, 'Direction must be a symbol.' unless value[:defaults][:direction].is_a?(Symbol) + + validate_array(value[:valid_associations]) + + end + end end \ No newline at end of file diff --git a/spec/lib/filter/query_spec.rb b/spec/lib/filter/query_spec.rb index c888b457..bae58949 100644 --- a/spec/lib/filter/query_spec.rb +++ b/spec/lib/filter/query_spec.rb @@ -308,13 +308,13 @@ def create_filter(params) end it 'occurs with a deformed \'in\' filter' do - filter_params = {'filter' => {'siteId' => {'in' => [{'customLatitude' => nil,'customLongitude' => nil,'description' => nil,'id' => 508,'locationObfuscated' => true,'name' => 'Site 1','projectIds' => [397],'links' => ['http://example.com/projects/397/sites/508']},{'customLatitude' => nil,'customLongitude' => nil,'description' => nil,'id' => 400,'locationObfuscated' => true,'name' => 'Site 2','projectIds' => [397],'links' => ['http://example.com/projects/397/sites/400']},{'customLatitude' => nil,'customLongitude' => nil,'description' => nil,'id' => 402,'locationObfuscated' => true,'name' => 'Site 3','projectIds' => [397],'links' => ['http://example.com/projects/397/sites/402']},{'customLatitude' => nil,'customLongitude' => nil,'description' => nil,'id' => 399,'locationObfuscated' => true,'name' => 'Site 4','projectIds' => [397,469],'links' => ['http://example.com/projects/397/sites/399','http://example.com/projects/469/sites/399']},{'customLatitude' => nil,'customLongitude' => nil,'description' => nil,'id' => 401,'locationObfuscated' => true,'name' => 'Site 5','projectIds' => [397],'links' => ['http://example.com/projects/397/sites/401']},{'customLatitude' => nil,'customLongitude' => nil,'description' => nil,'id' => 398,'locationObfuscated' => true,'name' => 'Site 6','projectIds' => [397,469],'links' => ['http://example.com/projects/397/sites/398','http://example.com/projects/469/sites/398']}]}},'projection' => {'include' => ['id','siteId','durationSeconds','recordedDate']}} - + filter_params = {'filter' => {'siteId' => {'in' => [{'customLatitude' => nil, 'customLongitude' => nil, 'description' => nil, 'id' => 508, 'locationObfuscated' => true, 'name' => 'Site 1', 'projectIds' => [397], 'links' => ['http://example.com/projects/397/sites/508']}, {'customLatitude' => nil, 'customLongitude' => nil, 'description' => nil, 'id' => 400, 'locationObfuscated' => true, 'name' => 'Site 2', 'projectIds' => [397], 'links' => ['http://example.com/projects/397/sites/400']}, {'customLatitude' => nil, 'customLongitude' => nil, 'description' => nil, 'id' => 402, 'locationObfuscated' => true, 'name' => 'Site 3', 'projectIds' => [397], 'links' => ['http://example.com/projects/397/sites/402']}, {'customLatitude' => nil, 'customLongitude' => nil, 'description' => nil, 'id' => 399, 'locationObfuscated' => true, 'name' => 'Site 4', 'projectIds' => [397, 469], 'links' => ['http://example.com/projects/397/sites/399', 'http://example.com/projects/469/sites/399']}, {'customLatitude' => nil, 'customLongitude' => nil, 'description' => nil, 'id' => 401, 'locationObfuscated' => true, 'name' => 'Site 5', 'projectIds' => [397], 'links' => ['http://example.com/projects/397/sites/401']}, {'customLatitude' => nil, 'customLongitude' => nil, 'description' => nil, 'id' => 398, 'locationObfuscated' => true, 'name' => 'Site 6', 'projectIds' => [397, 469], 'links' => ['http://example.com/projects/397/sites/398', 'http://example.com/projects/469/sites/398']}]}}, 'projection' => {'include' => ['id', 'siteId', 'durationSeconds', 'recordedDate']}} + expect { create_filter(filter_params).query_full }.to raise_error(CustomErrors::FilterArgumentError, 'Array values cannot be hashes.') end - + end context 'projection' do @@ -358,7 +358,7 @@ def create_filter(params) } complex_result = "SELECT\"audio_recordings\".\"id\", \ -\"audio_recordings\".\"duration_seconds\" \ + \"audio_recordings\".\"duration_seconds\" \ FROM\"audio_recordings\" \ WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ AND\"audio_recordings\".\"site_id\"=5 \ @@ -484,7 +484,7 @@ def create_filter(params) complex_result = "SELECT\"audio_recordings\".\"recorded_date\",\"audio_recordings\".\"site_id\", \ -\"audio_recordings\".\"duration_seconds\",\"audio_recordings\".\"media_type\" \ + \"audio_recordings\".\"duration_seconds\",\"audio_recordings\".\"media_type\" \ FROM\"audio_recordings\"WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ AND(\"audio_recordings\".\"site_id\"<123456 \ AND\"audio_recordings\".\"site_id\">9876 \ @@ -522,7 +522,7 @@ def create_filter(params) complex_result_2 = "SELECT\"audio_recordings\".\"recorded_date\",\"audio_recordings\".\"site_id\", \ -\"audio_recordings\".\"duration_seconds\",\"audio_recordings\".\"media_type\" \ + \"audio_recordings\".\"duration_seconds\",\"audio_recordings\".\"media_type\" \ FROM\"audio_recordings\" \ INNERJOIN\"sites\"ON\"sites\".\"id\"=\"audio_recordings\".\"site_id\"AND(\"sites\".\"deleted_at\"ISNULL) \ INNERJOIN\"projects_sites\"ON\"projects_sites\".\"site_id\"=\"sites\".\"id\" \ @@ -559,7 +559,6 @@ def create_filter(params) ORDERBY\"audio_recordings\".\"duration_seconds\"DESCLIMIT10OFFSET0" - filter_query = Filter::Query.new( complex_sample, Access::Query.audio_recordings(user, Access::Core.levels_allow), @@ -571,4 +570,35 @@ def create_filter(params) end end + + context 'with joins' do + + it 'simple audio_recordings query' do + request_body_obj = { + projection: { + exclude: [ + :uuid, :recorded_date, :site_id, + :sample_rate_hertz, :channels, :bit_rate_bps, :media_type, + :data_length_bytes, :status, :created_at, :updated_at + ] + }, + filter: { + 'site_id' => { + eq: 5 + }, + 'audio_events.is_reference' => { + eq: true + } + } + } + complex_result = + "SELECT\"audio_recordings\".\"id\", \ + \"audio_recordings\".\"duration_seconds\" \ +FROM\"audio_recordings\" \ +WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ +AND\"audio_recordings\".\"site_id\"=5 \ +ORDERBY\"audio_recordings\".\"recorded_date\"DESCLIMIT25OFFSET0" + compare_filter_sql(request_body_obj, complex_result) + end + end end diff --git a/spec/routing/audio_event_comments_routing_spec.rb b/spec/routing/audio_event_comments_routing_spec.rb index ed46c364..77ed6ff6 100644 --- a/spec/routing/audio_event_comments_routing_spec.rb +++ b/spec/routing/audio_event_comments_routing_spec.rb @@ -11,7 +11,6 @@ it { expect(get('/audio_events/1/comments/2/edit')).to route_to('errors#route_error', requested_route: 'audio_events/1/comments/2/edit') } - it { expect(get('/audio_events/1/comments/2')).to route_to('audio_event_comments#show', audio_event_id: '1', id: '2', format: 'json') } it { expect(put('/audio_events/1/comments/2')).to route_to('audio_event_comments#update', audio_event_id: '1', id: '2', format: 'json') } it { expect(delete('/audio_events/1/comments/2')).to route_to('audio_event_comments#destroy', audio_event_id: '1', id: '2', format: 'json') } @@ -19,5 +18,9 @@ it { expect(get('/audio_events/1/comments/filter')).to route_to('audio_event_comments#filter',audio_event_id: '1', format: 'json') } it { expect(post('/audio_events/1/comments/filter')).to route_to('audio_event_comments#filter', audio_event_id: '1', format: 'json') } + + # used by client + it { expect(get('/audio_events/1/comments/2')).to route_to('audio_event_comments#show', audio_event_id: '1', id: '2', format: 'json') } + end end diff --git a/spec/routing/audio_events_routing_spec.rb b/spec/routing/audio_events_routing_spec.rb index 01c30e26..dc708c50 100644 --- a/spec/routing/audio_events_routing_spec.rb +++ b/spec/routing/audio_events_routing_spec.rb @@ -11,22 +11,24 @@ it { expect(put('/projects/1/sites/2/audio_recordings/3/audio_events/4')).to route_to('errors#route_error', requested_route: 'projects/1/sites/2/audio_recordings/3/audio_events/4') } it { expect(delete('/projects/1/sites/2/audio_recordings/3/audio_events/4')).to route_to('errors#route_error', requested_route: 'projects/1/sites/2/audio_recordings/3/audio_events/4') } - - it { expect(get('/audio_recordings/3/audio_events/download')).to route_to('audio_events#download', audio_recording_id: '3', format: 'csv') } - - it { expect(get('/audio_recordings/3/audio_events')).to route_to('audio_events#index', audio_recording_id: '3', format: 'json') } it { expect(post('/audio_recordings/3/audio_events')).to route_to('audio_events#create', audio_recording_id: '3', format: 'json') } it { expect(get('/audio_recordings/3/audio_events/new')).to route_to('audio_events#new', audio_recording_id: '3', format: 'json') } it { expect(get('/audio_recordings/3/audio_events/4/edit')).to route_to('errors#route_error', requested_route: 'audio_recordings/3/audio_events/4/edit') } - it { expect(get('/audio_recordings/3/audio_events/4')).to route_to('audio_events#show', audio_recording_id: '3', id: '4', format: 'json') } it { expect(put('/audio_recordings/3/audio_events/4')).to route_to('audio_events#update', audio_recording_id: '3', id: '4', format: 'json') } it { expect(delete('/audio_recordings/3/audio_events/4')).to route_to('audio_events#destroy', audio_recording_id: '3', id: '4', format: 'json') } - it { expect(get('/audio_events/library')).to route_to('audio_events#library', format: 'json') } it { expect(get('/audio_events/new')).to route_to('errors#route_error', requested_route: 'audio_events/new') } it { expect(get '/projects/1/audio_events/download').to route_to('audio_events#download', project_id: '1', format: 'csv') } it { expect(get '/projects/1/sites/2/audio_events/download').to route_to('audio_events#download', project_id: '1', site_id: '2', format: 'csv') } + # used by client + it { expect(get('/audio_recordings/3/audio_events')).to route_to('audio_events#index', audio_recording_id: '3', format: 'json') } + it { expect(get('/audio_recordings/3/audio_events/4')).to route_to('audio_events#show', audio_recording_id: '3', id: '4', format: 'json') } + it { expect(get('/audio_recordings/3/audio_events/download')).to route_to('audio_events#download', audio_recording_id: '3', format: 'csv') } + it { expect(get('/audio_recordings/3/audio_events/download.csv')).to route_to('audio_events#download', audio_recording_id: '3', format: 'csv') } + + it { expect(get('/audio_events/library')).to route_to('audio_events#library', format: 'json') } + it { expect(get('/audio_events/library/paged')).to route_to('audio_events#library_paged', format: 'json') } end end \ No newline at end of file diff --git a/spec/routing/audio_recordings_routing_spec.rb b/spec/routing/audio_recordings_routing_spec.rb index a4f49f50..3ee33045 100644 --- a/spec/routing/audio_recordings_routing_spec.rb +++ b/spec/routing/audio_recordings_routing_spec.rb @@ -3,19 +3,27 @@ describe AudioRecordingsController, :type => :routing do describe :routing do - it { expect(get('/projects/1/sites/2/audio_recordings/check_uploader/4')).to route_to('audio_recordings#check_uploader', project_id: '1', site_id: '2', uploader_id: '4', format: 'json') } - it { expect(get('/projects/1/sites/2/audio_recordings')).to route_to('errors#route_error', requested_route: 'projects/1/sites/2/audio_recordings') } - it { expect(post('/projects/1/sites/2/audio_recordings')).to route_to('audio_recordings#create', project_id: '1', site_id: '2', format: 'json') } + it { expect(get('/projects/1/sites/2/audio_recordings/new')).to route_to('audio_recordings#new', project_id: '1', site_id: '2', format: 'json') } it { expect(get('/projects/1/sites/2/audio_recordings/3')).to route_to('errors#route_error', requested_route:'projects/1/sites/2/audio_recordings/3' ) } - it { expect(get('audio_recordings/3')).to route_to('audio_recordings#show', id: '3', format: 'json') } - it { expect(get('audio_recordings/new')).to route_to('audio_recordings#new', format: 'json') } + it { expect(get('/audio_recordings/new')).to route_to('audio_recordings#new', format: 'json') } + + + + # used by harvester + it { expect(get('/audio_recordings/3')).to route_to('audio_recordings#show', id: '3', format: 'json') } + it { expect(post('/projects/1/sites/2/audio_recordings')).to route_to('audio_recordings#create', project_id: '1', site_id: '2', format: 'json') } + it { expect(get('/projects/1/sites/2/audio_recordings/check_uploader/4')).to route_to('audio_recordings#check_uploader', project_id: '1', site_id: '2', uploader_id: '4', format: 'json') } it { expect(put('/audio_recordings/3/update_status')).to route_to('audio_recordings#update_status', :id => '3', format: 'json') } - it { expect(get('audio_recordings/filter')).to route_to('audio_recordings#filter', format: 'json') } - it { expect(post('audio_recordings/filter')).to route_to('audio_recordings#filter', format: 'json') } + # used by client + # /audio_recordings/3 + it { expect(get('/audio_recordings')).to route_to('audio_recordings#index', format: 'json') } + + it { expect(get('/audio_recordings/filter')).to route_to('audio_recordings#filter', format: 'json') } + it { expect(post('/audio_recordings/filter')).to route_to('audio_recordings#filter', format: 'json') } end end \ No newline at end of file diff --git a/spec/routing/bookmarks_routing_spec.rb b/spec/routing/bookmarks_routing_spec.rb index dce70cea..198c73fc 100644 --- a/spec/routing/bookmarks_routing_spec.rb +++ b/spec/routing/bookmarks_routing_spec.rb @@ -6,11 +6,14 @@ it { expect(get('/bookmarks')).to route_to('bookmarks#index') } it { expect(post('/bookmarks')).to route_to('bookmarks#create') } it { expect(get('/bookmarks/new')).to route_to('bookmarks#new') } - it { expect(get('/bookmarks/1')).to route_to('bookmarks#show', id: '1') } it { expect(put('/bookmarks/1')).to route_to('bookmarks#update', id: '1') } it { expect(delete('/bookmarks/1')).to route_to('bookmarks#destroy', id: '1') } it { expect(get('bookmarks/filter')).to route_to('bookmarks#filter', format: 'json') } it { expect(post('bookmarks/filter')).to route_to('bookmarks#filter', format: 'json') } + + # used by client + it { expect(get('/bookmarks/1')).to route_to('bookmarks#show', id: '1') } + end end \ No newline at end of file diff --git a/spec/routing/devise_routing_spec.rb b/spec/routing/devise_routing_spec.rb index 7834dbfc..8f4c1f85 100644 --- a/spec/routing/devise_routing_spec.rb +++ b/spec/routing/devise_routing_spec.rb @@ -28,8 +28,12 @@ it { expect(get('/my_account/unlock')).to route_to('devise/unlocks#show') } it { expect(get('/security/new')).to route_to('sessions#new', format: 'json') } - it { expect(post('/security')).to route_to('sessions#create', format: 'json') } it { expect(delete('/security')).to route_to('sessions#destroy', format: 'json') } + + # used by harvester + it { expect(post('/security')).to route_to('sessions#create', format: 'json') } + + # used by client it { expect(get('/security/user')).to route_to('sessions#show', format: 'json') } end diff --git a/spec/routing/media_routing_spec.rb b/spec/routing/media_routing_spec.rb index c3462917..8e294a68 100644 --- a/spec/routing/media_routing_spec.rb +++ b/spec/routing/media_routing_spec.rb @@ -2,7 +2,7 @@ describe MediaController, :type => :routing do describe :routing do - + # also used by client it { expect(get('/projects/1/sites/2/audio_recordings/3/media.json')).to route_to('errors#route_error', requested_route: 'projects/1/sites/2/audio_recordings/3/media', format: 'json') } it { expect(get('/projects/1/sites/2/audio_recordings/3/media.png')).to route_to('errors#route_error', requested_route: 'projects/1/sites/2/audio_recordings/3/media', format: 'png') } it { expect(get('/projects/1/sites/2/audio_recordings/3/media.mp3')).to route_to('errors#route_error', requested_route: 'projects/1/sites/2/audio_recordings/3/media', format: 'mp3') } diff --git a/spec/routing/projects_routing_spec.rb b/spec/routing/projects_routing_spec.rb index 21406a7b..8d4e45df 100644 --- a/spec/routing/projects_routing_spec.rb +++ b/spec/routing/projects_routing_spec.rb @@ -7,13 +7,17 @@ it { expect(get('/projects/new_access_request')).to route_to('projects#new_access_request') } it { expect(post('/projects/submit_access_request')).to route_to('projects#submit_access_request') } - it { expect(get('/projects')).to route_to('projects#index') } it { expect(post('/projects')).to route_to('projects#create') } it { expect(get('/projects/new')).to route_to('projects#new') } it { expect(get('/projects/1/edit')).to route_to('projects#edit', id: '1') } - it { expect(get('/projects/1')).to route_to('projects#show', id: '1') } it { expect(put('/projects/1')).to route_to('projects#update', id: '1') } it { expect(delete('/projects/1')).to route_to('projects#destroy', id: '1') } + # used by client + it { expect(get('/projects')).to route_to('projects#index') } + it { expect(get('/projects/1')).to route_to('projects#show', id: '1') } + it { expect(get('/projects/filter')).to route_to('projects#filter', format: 'json') } + it { expect(post('/projects/filter')).to route_to('projects#filter', format: 'json') } + end end \ No newline at end of file diff --git a/spec/routing/sites_routing_spec.rb b/spec/routing/sites_routing_spec.rb index 691d5acb..f49077d9 100644 --- a/spec/routing/sites_routing_spec.rb +++ b/spec/routing/sites_routing_spec.rb @@ -9,13 +9,14 @@ it { expect(post('projects/1/sites')).to route_to('sites#create', project_id: '1') } it { expect(get('projects/1/sites/new')).to route_to('sites#new', project_id: '1') } it { expect(get('projects/1/sites/1/edit')).to route_to('sites#edit', id: '1', project_id: '1') } - it { expect(get('projects/1/sites/1')).to route_to('sites#show', id: '1', project_id: '1') } + it { expect(put('projects/1/sites/1')).to route_to('sites#update', id: '1', project_id: '1') } it { expect(delete('projects/1/sites/1')).to route_to('sites#destroy', id: '1', project_id: '1') } + # used by client it { expect(get('projects/1/sites')).to route_to('sites#index', project_id: '1', format: 'json') } - it { expect(get('sites/1')).to route_to('sites#show_shallow', id: '1', format: 'json') } + it { expect(get('projects/1/sites/1')).to route_to('sites#show', id: '1', project_id: '1') } it { expect(get('sites/filter')).to route_to('sites#filter', format: 'json') } it { expect(post('sites/filter')).to route_to('sites#filter', format: 'json') } diff --git a/spec/routing/taggings_routing_spec.rb b/spec/routing/taggings_routing_spec.rb index 751a36cb..0d2d88b1 100644 --- a/spec/routing/taggings_routing_spec.rb +++ b/spec/routing/taggings_routing_spec.rb @@ -11,14 +11,17 @@ it { expect(put('/projects/1/sites/2/audio_recordings/3/audio_events/4/taggings/5')).to route_to('errors#route_error', requested_route: 'projects/1/sites/2/audio_recordings/3/audio_events/4/taggings/5') } it { expect(delete('/projects/1/sites/2/audio_recordings/3/audio_events/4/taggings/5')).to route_to('errors#route_error', requested_route: 'projects/1/sites/2/audio_recordings/3/audio_events/4/taggings/5') } - it { expect(get('/audio_recordings/3/audio_events/4/taggings')).to route_to('taggings#index', audio_recording_id: '3', audio_event_id: '4', format: 'json') } + it { expect(post('/audio_recordings/3/audio_events/4/taggings')).to route_to('taggings#create', audio_recording_id: '3', audio_event_id: '4', format: 'json') } it { expect(get('/audio_recordings/3/audio_events/4/taggings/new')).to route_to('taggings#new', audio_recording_id: '3', audio_event_id: '4', format: 'json') } it { expect(get('/audio_recordings/3/audio_events/4/taggings/5/edit')).to route_to('errors#route_error', requested_route: 'audio_recordings/3/audio_events/4/taggings/5/edit') } - it { expect(get('/audio_recordings/3/audio_events/4/taggings/5')).to route_to('taggings#show', audio_recording_id: '3', audio_event_id: '4', id: '5', format: 'json') } it { expect(put('/audio_recordings/3/audio_events/4/taggings/5')).to route_to('taggings#update', audio_recording_id: '3', audio_event_id: '4', id: '5', format: 'json') } it { expect(delete('/audio_recordings/3/audio_events/4/taggings/5')).to route_to('taggings#destroy', audio_recording_id: '3', audio_event_id: '4', id: '5', format: 'json') } it { expect(get('/taggings/user/1/tags')).to route_to('taggings#user_index', user_id: '1', format: 'json') } + + # used by client + it { expect(get('/audio_recordings/3/audio_events/4/taggings')).to route_to('taggings#index', audio_recording_id: '3', audio_event_id: '4', format: 'json') } + it { expect(get('/audio_recordings/3/audio_events/4/taggings/5')).to route_to('taggings#show', audio_recording_id: '3', audio_event_id: '4', id: '5', format: 'json') } end end \ No newline at end of file diff --git a/spec/routing/tags_routing_spec.rb b/spec/routing/tags_routing_spec.rb index bf170b04..adf36477 100644 --- a/spec/routing/tags_routing_spec.rb +++ b/spec/routing/tags_routing_spec.rb @@ -6,14 +6,18 @@ it { expect(get('/projects/1/sites/2/audio_recordings/3/audio_events/4/tags')).to route_to('errors#route_error', requested_route: 'projects/1/sites/2/audio_recordings/3/audio_events/4/tags') } it { expect(get('audio_recordings/3/audio_events/4/tags')).to route_to('tags#index', audio_recording_id: '3', audio_event_id: '4', format: 'json') } - it { expect(get('/tags')).to route_to('tags#index', format: 'json') } it { expect(post('/tags')).to route_to('tags#create', format: 'json') } it { expect(get('/tags/new')).to route_to('tags#new', format: 'json') } it { expect(get('/tags/1/edit')).to route_to('errors#route_error', requested_route: 'tags/1/edit') } - it { expect(get('/tags/1')).to route_to('tags#show', id: '1', format: 'json') } + it { expect(put('/tags/1')).to route_to('errors#route_error', requested_route: 'tags/1') } it { expect(delete('/tags/1')).to route_to('errors#route_error', requested_route: 'tags/1') } it { expect(get('/tags?filter=koala,bellow')).to route_to('tags#index', format: 'json', filter: 'koala,bellow')} + + # used by client + it { expect(get('/tags')).to route_to('tags#index', format: 'json') } + it { expect(get('/tags/1')).to route_to('tags#show', id: '1', format: 'json') } + end end \ No newline at end of file diff --git a/spec/routing/user_accounts_routing_spec.rb b/spec/routing/user_accounts_routing_spec.rb index b5448f7c..174e4e08 100644 --- a/spec/routing/user_accounts_routing_spec.rb +++ b/spec/routing/user_accounts_routing_spec.rb @@ -11,6 +11,7 @@ it { expect(put('/user_accounts/1')).to route_to('user_accounts#update', id: '1') } it { expect(delete('/user_accounts/1')).to route_to('errors#route_error', requested_route: 'user_accounts/1') } + # used by client it { expect(get('/my_account/')).to route_to('user_accounts#my_account') } it { expect(put('/my_account/prefs')).to route_to('user_accounts#modify_preferences') } From 4b856a1821ef68430ad0312697c05bdbe754903a Mon Sep 17 00:00:00 2001 From: cofiem Date: Tue, 17 Mar 2015 16:29:58 +1000 Subject: [PATCH 38/49] more work on enhancing filter for #176 --- lib/modules/filter/build.rb | 550 +++++++++++++++++---------------- lib/modules/filter/query.rb | 63 ++-- lib/modules/filter/validate.rb | 18 +- spec/lib/filter/query_spec.rb | 34 +- spec/spec_helper.rb | 6 +- 5 files changed, 373 insertions(+), 298 deletions(-) diff --git a/lib/modules/filter/build.rb b/lib/modules/filter/build.rb index 3483ebd5..3d5b09af 100644 --- a/lib/modules/filter/build.rb +++ b/lib/modules/filter/build.rb @@ -1,111 +1,90 @@ -require 'active_support/concern' - module Filter - # Provides support for parsing a query from a hash. - module Build - extend ActiveSupport::Concern - extend Comparison - extend Core - extend Subset - extend Validate - extend Projection - extend Custom - - private + # Provides support for parsing a filter from a hash to build a query. + class Build + include Comparison + include Core + include Custom + include Projection + include Subset + include Validate - # Build conditions from a hash. - # @param [Hash] hash + # Create an instance of Build. # @param [Arel::Table] table # @param [Hash] filter_settings - # @return [Array] conditions - def build_top(hash, table, filter_settings) - fail CustomErrors::FilterArgumentError.new("Conditions hash must have at least 1 entry, got #{hash.size}.", {hash: hash}) if hash.blank? || hash.size < 1 - conditions = [] - hash.each do |key, value| - # combinators or fields can be at top level. Assumes 'and' (query.where(condition) uses 'and'). - - built_conditions = build_hash(key, value, table, filter_settings) - conditions.push(*built_conditions) - end - conditions + # @return [void] + def initialize(table, filter_settings) + @table = table + @filter_settings = filter_settings + + @valid_fields = filter_settings[:valid_fields].map(&:to_sym) + @text_fields = filter_settings[:text_fields].map(&:to_sym) + @valid_associations = filter_settings[:valid_associations] + + @valid_conditions = [ + # comparison + :eq, :equal, + :not_eq, :not_equal, + :lt, :less_than, + :not_lt, :not_less_than, + :gt, :greater_than, + :not_gt, :not_greater_than, + :lteq, :less_than_or_equal, + :not_lteq, :not_less_than_or_equal, + :gteq, :greater_than_or_equal, + :not_gteq, :not_greater_than_or_equal, + + # subset + :range, :in_range, + :not_range, :not_in_range, + :in, + :not_in, + :contains, :contain, + :not_contains, :not_contain, :does_not_contain, + :starts_with, :start_with, + :not_starts_with, :not_start_with, :does_not_start_with, + :ends_with, :end_with, + :not_ends_with, :not_end_with, :does_not_end_with, + :regex + ] end - # Build conditions from a hash. - # @param [Symbol] field - # @param [Hash] hash - # @param [Arel::Table] table - # @param [Hash] filter_settings - # @return [Array] conditions - def build_hash(field, hash, table, filter_settings) - fail CustomErrors::FilterArgumentError.new("Conditions hash must have at least 1 entry, got #{hash.size}.", {field: field, hash: hash}) if hash.blank? || hash.size < 1 - fail CustomErrors::FilterArgumentError.new("'Not' must have a single combiner or field name, got #{hash.size}", {field: field, hash: hash}) if field == :not && hash.size != 1 - conditions = [] - - case field - when :and, :or - conditions_to_combine = build_hashes(hash, table, filter_settings) - combined_conditions = build_combiner(field, conditions_to_combine) - conditions.push(combined_conditions) - else - hash.each do |key, value| - conditions.push(build_field(field, key, value, table, filter_settings)) - end - end - - conditions - end - - # Build conditions from nested hashes. + # Build projections from a hash. # @param [Hash] hash - # @param [Arel::Table] table - # @param [Hash] filter_settings - # @return [Array] conditions - def build_hashes(hash, table, filter_settings) - conditions = [] + # @return [Array] projections + def projections(hash) + fail CustomErrors::FilterArgumentError.new("Projections hash must have exactly 1 entry, got #{hash.size}.", {hash: hash}) if hash.blank? || hash.size != 1 + result = [] hash.each do |key, value| - built_conditions = build_hash(key, value, table, filter_settings) - conditions.push(*built_conditions) + fail CustomErrors::FilterArgumentError.new("Must be 'include' or 'exclude' at top level, got #{key}", {hash: hash}) unless [:include, :exclude].include?(key) + result = projection(key, value) end - conditions + result end - # Build a field condition. - # @param [Symbol] field + # Build projection to include or exclude. # @param [Symbol] key - # @param [Object] value - # @param [Arel::Table] table - # @param [Hash] filter_settings - # @return [Arel::Nodes::Node] condition - def build_field(field, key, value, table, filter_settings) - valid_fields = filter_settings[:valid_fields].map(&:to_sym) - case field - when :not - build_not(key, value, table, filter_settings) - when *valid_fields - build_condition(field, key, value, table, filter_settings) - when /\./ - table_mod, field_mod, filter_settings_mod = build_table_field(table, field, filter_settings) - build_condition(field_mod, key, value, table_mod, filter_settings_mod) - else - fail CustomErrors::FilterArgumentError.new("Unrecognised combiner or field name: #{field}.") - end - end + # @param [Hash] value + # @return [Array] projections + def projection(key, value) + fail CustomErrors::FilterArgumentError.new('Must not contain duplicate fields.', {"#{key}" => value}) if !value.blank? && value.uniq.length != value.length - # Build multiple combiners or field conditions. - # @param [Symbol] field - # @param [Symbol] key - # @param [Object] value - # @param [Arel::Table] table - # @param [Hash] filter_settings - # @return [Arel::Nodes::Node] condition - def build_multiple(field, key, value, table, filter_settings) - case field - when :and, :or - build_combiner(field, build_hash(key, value, table, filter_settings)) + columns = [] + case key + when :include + fail CustomErrors::FilterArgumentError.new('Include must contain at least one field.') if value.blank? + columns = value.map { |x| CleanParams.clean(x) } + when :exclude + fail CustomErrors::FilterArgumentError.new('Exclude must contain at least one field.') if value.blank? + columns = @valid_fields.reject { |item| value.include?(item) }.map { |x| CleanParams.clean(x) } + fail CustomErrors::FilterArgumentError.new('Exclude must contain at least one field.') if columns.blank? else - fail CustomErrors::FilterArgumentError.new("Unrecognised combiner or field name: #{field}", {field: key, hash: value}) + fail CustomErrors::FilterArgumentError.new("Unrecognised projection key #{key}.") end + + columns.map { |item| + project_column(@table, item, @valid_fields) + } end # Combine two conditions. @@ -113,7 +92,7 @@ def build_multiple(field, key, value, table, filter_settings) # @param [Arel::Nodes::Node] condition1 # @param [Arel::Nodes::Node] condition2 # @return [Arel::Nodes::Node] condition - def build_combiner_binary(combiner, condition1, condition2) + def combiner_two(combiner, condition1, condition2) case combiner when :and compose_and(condition1, condition2) @@ -128,7 +107,7 @@ def build_combiner_binary(combiner, condition1, condition2) # @param [Symbol] combiner # @param [Array] conditions # @return [Arel::Nodes::Node] condition - def build_combiner(combiner, conditions) + def combiner_one(combiner, conditions) fail CustomErrors::FilterArgumentError.new("Combiner '#{combiner}' must have at least 2 entries, got #{conditions.size}.") if conditions.blank? || conditions.size < 2 combined_conditions = nil @@ -137,7 +116,7 @@ def build_combiner(combiner, conditions) if combined_conditions.blank? combined_conditions = condition else - combined_conditions = build_combiner_binary(combiner, combined_conditions, condition) + combined_conditions = combiner_two(combiner, combined_conditions, condition) end end @@ -145,233 +124,245 @@ def build_combiner(combiner, conditions) combined_conditions end - # Build a not condition. - # @param [Symbol] field - # @param [Hash] hash - # @param [Arel::Table] table - # @param [Hash] filter_settings + # Build a text condition. + # @param [String] text # @return [Arel::Nodes::Node] condition - def build_not(field, hash, table, filter_settings) - fail CustomErrors::FilterArgumentError.new("'Not' must have a single filter, got #{hash.size}.", {field: field, hash: hash}) if hash.size != 1 - negated_condition = nil + def contains_text(text) + conditions = [] + @text_fields.each do |text_field| + condition = compose_contains(@table, text_field, @valid_fields, text) + conditions.push(condition) + end - hash.each do |key, value| - table_mod, field_mod, filter_settings_mod = build_table_field(table, field, filter_settings) - condition = build_condition(field_mod, key, value, table_mod, filter_settings_mod) - negated_condition = compose_not(condition) + if conditions.size > 1 + combiner_one(:or, conditions) + else + conditions[0] end + end + + # Build an equality condition that matches specified value to specified fields. + # @param [Hash] filter_hash + # @return [Arel::Nodes::Node] condition + def generic_equals(filter_hash) + conditions = [] + filter_hash.each do |key, value| + conditions.push(compose_eq(@table, key, @valid_fields, value)) + end + + if conditions.size > 1 + combiner_one(:and, conditions) + else + conditions[0] + end + + end + + # Parse a filter hash. + # @param [Hash] filter_hash + # @return [Hash] + def parse(filter_hash) + conditions, joins = parse_filter(filter_hash) + + # using .where in Rails will do 'AND' by default + #final_conditions = combiner_one(:and, conditions) + + # add joins to final_conditions + join_sql = [] + joins.each do |j| + left_outer_join = AudioRecording.arel_table.join(j[:join].arel_table, Arel::Nodes::OuterJoin).on(j[:on]) + sources = left_outer_join.join_sources + fail CustomErrors::FilterArgumentError.new("SQL contained more than one join: #{sources}") if sources.size != 1 + join_sql.push(sources[0].to_sql) + end + + [conditions, join_sql] + end + + private + + def parse_filter(primary, secondary = nil, extra = nil, joins = []) + + if primary.is_a?(Hash) + fail CustomErrors::FilterArgumentError.new("Filter hash must have at least 1 entry, got #{primary.size}.", {hash: primary}) if primary.blank? || primary.size < 1 + fail CustomErrors::FilterArgumentError.new("Extra must be null when processing a hash, got #{extra}.", {hash: primary}) unless extra.blank? + + conditions = [] + + primary.each do |key, value| + condition, joins = parse_filter(key, value, secondary, joins) + if condition.is_a?(Array) + conditions.push(*condition) + else + conditions.push(condition) + end + end - negated_condition + [conditions, joins] + + elsif primary.is_a?(Symbol) + + case primary + when :and, :or + combiner = primary + filter_hash = secondary + condition, joins = parse_filter(filter_hash, nil, nil, joins) + [combiner_one(combiner, condition), joins] + when :not + #combiner = primary + filter_hash = secondary + condition, joins = parse_filter(filter_hash, nil, nil, joins) + + if condition.respond_to?(:map) + negated_conditions = condition.map { |c| compose_not(c) } + else + negated_conditions = [compose_not(condition)] + end + + [negated_conditions, joins] + when *@valid_fields.dup.push(/\./) + field = primary + field_conditions = secondary + info = parse_table_field(@table, field, @filter_settings) + condition, joins = parse_filter(field_conditions, info, nil, joins) + [condition, joins] + when *@valid_conditions + filter_name = primary + filter_value = secondary + info = extra + + table = info[:arel_table] + column_name = info[:field_name] + model = info[:model] + valid_fields = info[:filter_settings][:valid_fields] + + # add join table to joins array if necessary + additional_joins, match = build_joins(model, @valid_associations) + + current_models = joins.map { |j| j[:join]} + new_joins = additional_joins.select { |j| !current_models.include?(j[:join]) } + joins.push(*new_joins) + + [condition(filter_name, table, column_name, valid_fields, filter_value), joins] + else + fail CustomErrors::FilterArgumentError.new("Unrecognised combiner or field name: #{primary}.") + end + else + fail CustomErrors::FilterArgumentError.new("Unrecognised filter component: #{primary}.") + end end # Build a condition. - # @param [Symbol] field # @param [Symbol] filter_name - # @param [Object] filter_value # @param [Arel::Table] table - # @param [Hash] filter_settings + # @param [Symbol] column_name + # @param [Array] valid_fields + # @param [Object] filter_value # @return [Arel::Nodes::Node] condition - def build_condition(field, filter_name, filter_value, table, filter_settings) - valid_fields = filter_settings[:valid_fields].map(&:to_sym) - special_condition = build_condition_special(field, filter_name, filter_value, table, valid_fields) - return special_condition unless special_condition.nil? - + def condition(filter_name, table, column_name, valid_fields, filter_value) case filter_name # comparisons when :eq, :equal - compose_eq(table, field, valid_fields, filter_value) + compose_eq(table, column_name, valid_fields, filter_value) when :not_eq, :not_equal - compose_not_eq(table, field, valid_fields, filter_value) + compose_not_eq(table, column_name, valid_fields, filter_value) when :lt, :less_than - compose_lt(table, field, valid_fields, filter_value) + compose_lt(table, column_name, valid_fields, filter_value) when :not_lt, :not_less_than - compose_not_lt(table, field, valid_fields, filter_value) + compose_not_lt(table, column_name, valid_fields, filter_value) when :gt, :greater_than - compose_gt(table, field, valid_fields, filter_value) + compose_gt(table, column_name, valid_fields, filter_value) when :not_gt, :not_greater_than - compose_not_gt(table, field, valid_fields, filter_value) + compose_not_gt(table, column_name, valid_fields, filter_value) when :lteq, :less_than_or_equal - compose_lteq(table, field, valid_fields, filter_value) + compose_lteq(table, column_name, valid_fields, filter_value) when :not_lteq, :not_less_than_or_equal - compose_not_lteq(table, field, valid_fields, filter_value) + compose_not_lteq(table, column_name, valid_fields, filter_value) when :gteq, :greater_than_or_equal - compose_gteq(table, field, valid_fields, filter_value) + compose_gteq(table, column_name, valid_fields, filter_value) when :not_gteq, :not_greater_than_or_equal - compose_not_gteq(table, field, valid_fields, filter_value) + compose_not_gteq(table, column_name, valid_fields, filter_value) # subsets when :range, :in_range - compose_range_options(table, field, valid_fields, filter_value) + compose_range_options(table, column_name, valid_fields, filter_value) when :not_range, :not_in_range - compose_not_range_options(table, field, valid_fields, filter_value) + compose_not_range_options(table, column_name, valid_fields, filter_value) when :in - compose_in(table, field, valid_fields, filter_value) + compose_in(table, column_name, valid_fields, filter_value) when :not_in - compose_not_in(table, field, valid_fields, filter_value) + compose_not_in(table, column_name, valid_fields, filter_value) when :contains, :contain - compose_contains(table, field, valid_fields, filter_value) + compose_contains(table, column_name, valid_fields, filter_value) when :not_contains, :not_contain, :does_not_contain - compose_not_contains(table, field, valid_fields, filter_value) + compose_not_contains(table, column_name, valid_fields, filter_value) when :starts_with, :start_with - compose_starts_with(table, field, valid_fields, filter_value) + compose_starts_with(table, column_name, valid_fields, filter_value) when :not_starts_with, :not_start_with, :does_not_start_with - compose_not_starts_with(table, field, valid_fields, filter_value) + compose_not_starts_with(table, column_name, valid_fields, filter_value) when :ends_with, :end_with - compose_ends_with(table, field, valid_fields, filter_value) + compose_ends_with(table, column_name, valid_fields, filter_value) when :not_ends_with, :not_end_with, :does_not_end_with - compose_not_ends_with(table, field, valid_fields, filter_value) + compose_not_ends_with(table, column_name, valid_fields, filter_value) when :regex - compose_regex(table, field, valid_fields, filter_value) + compose_regex(table, column_name, valid_fields, filter_value) + # unknown else fail CustomErrors::FilterArgumentError.new("Unrecognised filter #{filter_name}.") end end - # Build a text condition. - # @param [String] text - # @param [Array] text_fields - # @param [Arel::Table] table - # @param [Array] valid_fields - # @return [Arel::Nodes::Node] condition - def build_text(text, text_fields, table, valid_fields) - conditions = [] - text_fields.each do |text_field| - condition = compose_contains(table, text_field, valid_fields, text) - conditions.push(condition) - end - - if conditions.size > 1 - build_combiner(:or, conditions) - else - conditions[0] - end - end - - # Build an equality condition that matches specified value to specified fields. - # @param [Hash] filter_hash - # @param [Arel::Table] table - # @param [Array] valid_fields - # @return [Arel::Nodes::Node] condition - def build_generic(filter_hash, table, valid_fields) - conditions = [] - filter_hash.each do |key, value| - conditions.push(compose_eq(table, key, valid_fields, value)) - end - - if conditions.size > 1 - build_combiner(:and, conditions) - else - conditions[0] - end - - end - - # Build projections from a hash. - # @param [Hash] hash - # @param [Arel::Table] table - # @param [Array] valid_fields - # @return [Array] projections - def build_projections(hash, table, valid_fields) - fail CustomErrors::FilterArgumentError.new("Projections hash must have exactly 1 entry, got #{hash.size}.", {hash: hash}) if hash.blank? || hash.size != 1 - result = [] - hash.each do |key, value| - fail CustomErrors::FilterArgumentError.new("Must be 'include' or 'exclude' at top level, got #{key}", {hash: hash}) unless [:include, :exclude].include?(key) - result = build_projection(key, value, table, valid_fields) - end - result - end - - # Build projection to include or exclude. - # @param [Symbol] key - # @param [Hash] value - # @param [Arel::Table] table - # @param [Array] valid_fields - # @return [Array] projections - def build_projection(key, value, table, valid_fields) - fail CustomErrors::FilterArgumentError.new('Must not contain duplicate fields.', {"#{key}" => value}) if !value.blank? && value.uniq.length != value.length - - columns = [] - case key - when :include - fail CustomErrors::FilterArgumentError.new('Include must contain at least one field.') if value.blank? - columns = value.map { |x| CleanParams.clean(x) } - when :exclude - fail CustomErrors::FilterArgumentError.new('Exclude must contain at least one field.') if value.blank? - columns = valid_fields.reject { |item| value.include?(item) }.map { |x| CleanParams.clean(x) } - fail CustomErrors::FilterArgumentError.new('Exclude must contain at least one field.') if columns.blank? - else - fail CustomErrors::FilterArgumentError.new("Unrecognised projection key #{key}.") - end - - columns.map { |item| - project_column(table, item, valid_fields) - } - end - - # Build special project ids 'in' filter. - # @param [Symbol] field - # @param [Symbol] filter_name - # @param [Object] filter_value - # @param [Arel::Table] table - # @param [Array] valid_fields - # @return [Arel::Nodes::Node] condition - def build_condition_special(field, filter_name, filter_value, table, valid_fields) - # construct special conditions - if table.name == 'sites' && field == :project_ids - # filter by many-to-many projects <-> sites - fail CustomErrors::FilterArgumentError.new("Project_ids permits only 'in' filter, got #{filter_name}.") unless filter_name == :in - projects_sites_table = Arel::Table.new(:projects_sites) - special_value = Arel::Table.new(:projects_sites).project(:site_id).where(compose_in(projects_sites_table, :project_id, [:project_id], filter_value)) - compose_in(table, :id, valid_fields, special_value) - end - end - - def build_field_info(table_name, field_name) - model = table_name.to_s.classify.constantize - model_filter_settings = model.filter_settings - model_valid_fields = model_filter_settings[:valid_fields].map(&:to_sym) - field_sym = field_name.to_sym - arel_table = relation_table(model) - - validate_table_column(arel_table, field_sym, model_valid_fields) - - { - table_name: table_name, - field_name: field_sym, - arel_table: arel_table, - model: model, - filter_settings: model_filter_settings - } - end # Build table field from field symbol. # @param [Arel::Table] table # @param [Symbol] field # @param [Hash] filter_settings # @return [Arel::Table, Symbol, Hash] table, field, filter_settings - def build_table_field(table, field, filter_settings) + def parse_table_field(table, field, filter_settings) validate_table(table) fail CustomErrors::FilterArgumentError, 'Field name must be a symbol.' unless field.is_a?(Symbol) + validate_filter_settings(filter_settings) field_s = field.to_s if field_s.include?('.') dot_index = field.to_s.index('.') - parsed_table = field[0, dot_index] - parsed_field = field[(dot_index + 1)..field.length] - - info = build_field_info(parsed_table, parsed_field) + parsed_table = field[0, dot_index].to_sym + parsed_field = field[(dot_index + 1)..field.length].to_sym - associations = build_associations(filter_settings[:valid_associations], table) + associations = build_associations(@valid_associations, table) models = associations.map { |a| a[:join] } + table_names = associations.map { |a| a[:join].table_name.to_sym } + + validate_name(parsed_table, table_names) + + model = parsed_table.to_s.classify.constantize + + validate_association(model, models) - validate_association(info[:model], models) + model_filter_settings = model.filter_settings + model_valid_fields = model_filter_settings[:valid_fields].map(&:to_sym) + arel_table = relation_table(model) - [info[:arel_table], info[:field_name], info[:filter_settings]] + validate_table_column(arel_table, parsed_field, model_valid_fields) + + { + table_name: parsed_table, + field_name: parsed_field, + arel_table: arel_table, + model: model, + filter_settings: model_filter_settings + } else - [table, field, filter_settings] + { + table_name: table.name, + field_name: field, + arel_table: table, + model: table.name.to_s.classify.constantize, + filter_settings: filter_settings + } end end @@ -397,10 +388,10 @@ def build_associations(valid_associations, table) if available associations.push( - { - join: join, - on: on - }) + { + join: join, + on: on + }) end end @@ -408,5 +399,32 @@ def build_associations(valid_associations, table) associations end + # Get only the relevant joins + # @param [ActiveRecord::Base] model + # @param [Hash] associations + # @param [Array] joins + # @return [Array, Boolean] joins, match + def build_joins(model, associations, joins = []) + + associations.each do |a| + model_join = a[:join] + model_on = a[:on] + + join = {join: model_join, on: model_on} + + return [[join], true] if model == model_join + + if a.include?(:associations) + assoc = a[:associations] + assoc_joins, match = build_joins(model, assoc, joins + [join]) + + return [[join] + assoc_joins, true] if match + end + + end + + [[], false] + end + end end \ No newline at end of file diff --git a/lib/modules/filter/query.rb b/lib/modules/filter/query.rb index 53f456f8..f0c189ee 100644 --- a/lib/modules/filter/query.rb +++ b/lib/modules/filter/query.rb @@ -6,13 +6,13 @@ class Query include Subset include Projection include Parse - include Build include Validate include Custom - attr_reader :key_prefix, :max_limit, :initial_query, :table, :valid_fields, :text_fields, :filter_settings, - :parameters, :filter, :projection, :projection_built, :qsp_text_filter, :qsp_generic_filters, - :paging, :sorting + attr_reader :key_prefix, :max_items, :initial_query, :table, + :valid_fields, :text_fields, :filter_settings, + :parameters, :filter, :projection, :qsp_text_filter, + :qsp_generic_filters, :paging, :sorting # Convert a json POST body to an arel query. # @param [Hash] parameters @@ -34,6 +34,8 @@ def initialize(parameters, query, model, filter_settings) @render_fields = filter_settings[:render_fields].map(&:to_sym) @filter_settings = filter_settings + @build = Build.new(@table, filter_settings) + @parameters = CleanParams.perform(parameters) validate_hash(@parameters) @@ -43,13 +45,11 @@ def initialize(parameters, query, model, filter_settings) @projection = @parameters[:projection] @projection = nil if @projection.blank? - if has_projection_params? - @projection_built = build_projections(@projection, @table, @valid_fields) - else - @projection_built = build_projections({include: @render_fields}, @table, @valid_fields) - end - @qsp_text_filter = parse_qsp_text(@parameters) + + + + @qsp_generic_filters = parse_qsp(nil, @parameters, @key_prefix, @valid_fields) @paging = parse_paging( @parameters, @@ -133,7 +133,14 @@ def query_without_filter_paging_sorting # @return [ActiveRecord::Relation] query def query_filter(query) if has_filter_params? - apply_conditions(query, build_top(@filter, @table, @filter_settings)) + conditions, joins = @build.parse(@filter) + + # add distinct if there are joins + query = query.distinct if joins.size > 0 + + query = apply_conditions(query, conditions) + query = apply_joins(query, joins) if joins.size > 0 + query else query end @@ -144,7 +151,7 @@ def query_filter(query) # @return [ActiveRecord::Relation] query def query_projection(query) if has_projection_params? - apply_projections(query, build_projections(@projection, @table, @valid_fields)) + apply_projections(query, @build.projections(@projection)) else query_projection_default(query) end @@ -155,14 +162,14 @@ def query_projection(query) # @param [Array] filter_projection # @return [ActiveRecord::Relation] query def query_projection_custom(query, filter_projection) - apply_projections(query, build_projections({include: filter_projection}, @table, @valid_fields)) + apply_projections(query, @build.projections({include: filter_projection})) end # Add default projections to a query. # @param [ActiveRecord::Relation] query # @return [ActiveRecord::Relation] query def query_projection_default(query) - apply_projections(query, build_projections({include: @render_fields}, @table, @valid_fields)) + apply_projections(query, @build.projections({include: @render_fields})) end # Add text filter to a query. @@ -171,7 +178,7 @@ def query_projection_default(query) def query_filter_text(query) return query unless has_qsp_text? # only text fields on the /filter model can be used - can't filter on other table fields - text_condition = build_text(@qsp_text_filter, @text_fields, @table, @valid_fields) + text_condition = @build.contains_text(@qsp_text_filter) apply_condition(query, text_condition) end @@ -181,7 +188,7 @@ def query_filter_text(query) # @return [ActiveRecord::Relation] query def query_filter_text_custom(query, filter_text) # only text fields on the /filter model can be used - can't filter on other table fields - text_condition = build_text(filter_text, @text_fields, @table, @valid_fields) + text_condition = @build.contains_text(filter_text) apply_condition(query, text_condition) end @@ -191,7 +198,7 @@ def query_filter_text_custom(query, filter_text) def query_filter_generic(query) return query unless has_qsp_generic? # only fields on the /filter model can be used - can't filter on other table fields - apply_condition(query, build_generic(@qsp_generic_filters, @table, @valid_fields)) + apply_condition(query, @build.generic_equals(@qsp_generic_filters)) end # Add generic equality filters to a query. @@ -200,7 +207,7 @@ def query_filter_generic(query) # @return [ActiveRecord::Relation] query def query_filter_generic_custom(query, filter_hash) # only fields on the /filter model can be used - can't filter on other table fields - apply_condition(query, build_generic(filter_hash, @table, @valid_fields)) + apply_condition(query, @build.generic_equals(filter_hash)) end # Add sorting to query. @@ -289,6 +296,26 @@ def apply_condition(query, condition) query.where(condition) end + # Add joins to a query. + # @param [ActiveRecord::Relation] query + # @param [Array] joins + # @return [ActiveRecord::Relation] the modified query + def apply_joins(query, joins) + joins.each do |join| + query = apply_join(query, join) + end + query + end + + # Add join to a query. + # @param [ActiveRecord::Relation] query + # @param [String] join + # @return [ActiveRecord::Relation] the modified query + def apply_join(query, join) + validate_query(query) + query.joins(join) + end + # Append sorting to a query. # @param [ActiveRecord::Relation] query # @param [Arel::Table] table diff --git a/lib/modules/filter/validate.rb b/lib/modules/filter/validate.rb index 38476346..1cb1fb37 100644 --- a/lib/modules/filter/validate.rb +++ b/lib/modules/filter/validate.rb @@ -63,7 +63,7 @@ def validate_integer(value, min = nil, max = nil) def validate_query_table_column(query, table, column_name, allowed) validate_query(query) validate_table(table) - validate_column_name(column_name, allowed) + validate_name(column_name, allowed) end # Validate table and column values. @@ -73,7 +73,7 @@ def validate_query_table_column(query, table, column_name, allowed) # @return [void] def validate_table_column(table, column_name, allowed) validate_table(table) - validate_column_name(column_name, allowed) + validate_name(column_name, allowed) end def validate_association(model, models_allowed) @@ -126,16 +126,16 @@ def validate_projection(projection) fail CustomErrors::FilterArgumentError, "Condition must be Arel::Attributes::Attribute, got #{projection}" unless projection.is_a?(Arel::Attributes::Attribute) end - # Validate column name value. - # @param [Symbol] column_name + # Validate name value. + # @param [Symbol] name # @param [Array] allowed - # @raise [FilterArgumentError] if column name is not a symbol in allowed + # @raise [FilterArgumentError] if name is not a symbol in allowed # @return [void] - def validate_column_name(column_name, allowed) - fail CustomErrors::FilterArgumentError, "Column name must not be null, got #{column_name}" if column_name.blank? - fail CustomErrors::FilterArgumentError, "Column name must be a symbol, got #{column_name}" unless column_name.is_a?(Symbol) + def validate_name(name, allowed) + fail CustomErrors::FilterArgumentError, "Name must not be null, got #{name}" if name.blank? + fail CustomErrors::FilterArgumentError, "Name must be a symbol, got #{name}" unless name.is_a?(Symbol) fail CustomErrors::FilterArgumentError, "Allowed must be an Array, got #{allowed}" unless allowed.is_a?(Array) - fail CustomErrors::FilterArgumentError, "Column name must be in #{allowed}, got #{column_name}" unless allowed.include?(column_name) + fail CustomErrors::FilterArgumentError, "Name must be in #{allowed}, got #{name}" unless allowed.include?(name) end # Validate model value. diff --git a/spec/lib/filter/query_spec.rb b/spec/lib/filter/query_spec.rb index bae58949..804c72dd 100644 --- a/spec/lib/filter/query_spec.rb +++ b/spec/lib/filter/query_spec.rb @@ -583,22 +583,52 @@ def create_filter(params) ] }, filter: { - 'site_id' => { + 'sites.id' => { eq: 5 }, 'audio_events.is_reference' => { eq: true + }, + 'tags.text' => { + contains: 'koala' } } } complex_result = + "SELECTDISTINCT\"audio_recordings\".\"id\",\"audio_recordings\".\"duration_seconds\" \ +FROM\"audio_recordings\" \ +LEFTOUTERJOIN\"audio_events\"ON\"audio_recordings\".\"id\"=\"audio_events\".\"audio_recording_id\" \ +LEFTOUTERJOIN\"audio_events_tags\"ON\"audio_events\".\"id\"=\"audio_events_tags\".\"audio_event_id\" \ +LEFTOUTERJOIN\"tags\"ON\"audio_events_tags\".\"tag_id\"=\"tags\".\"id\" \ +WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ +AND\"sites\".\"id\"=5 \ +AND\"audio_events\".\"is_reference\"='t' \ +AND(\"tags\".\"text\"ILIKE'%koala%') \ +ORDERBY\"audio_recordings\".\"recorded_date\"DESC \ +LIMIT25OFFSET0" + compare_filter_sql(request_body_obj, complex_result) + + @permission = FactoryGirl.create(:write_permission) + user = @permission.user + user_id = user.id + + complex_result_2 = "SELECT\"audio_recordings\".\"id\", \ \"audio_recordings\".\"duration_seconds\" \ FROM\"audio_recordings\" \ WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ AND\"audio_recordings\".\"site_id\"=5 \ ORDERBY\"audio_recordings\".\"recorded_date\"DESCLIMIT25OFFSET0" - compare_filter_sql(request_body_obj, complex_result) + + filter_query = Filter::Query.new( + request_body_obj, + Access::Query.audio_recordings(user, Access::Core.levels_allow), + AudioRecording, + AudioRecording.filter_settings + ) + + expect(filter_query.query_full.to_sql.gsub(/\s+/, '')).to eq(complex_result_2.gsub(/\s+/, '')) + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6230c1d9..2f63f13f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -99,9 +99,9 @@ DatabaseCleaner.clean_with(:truncation) begin DatabaseCleaner.start - puts '===> FactoryGirl lint started.' - FactoryGirl.lint - puts '===> FactoryGirl lint completed.' + #puts '===> FactoryGirl lint started.' + #FactoryGirl.lint + #puts '===> FactoryGirl lint completed.' ensure DatabaseCleaner.clean puts '===> Database cleaner run.' From 2291c8855a9820810161db87d5246faa0728120d Mon Sep 17 00:00:00 2001 From: cofiem Date: Tue, 17 Mar 2015 20:36:00 +1000 Subject: [PATCH 39/49] Completed adding abilkity to filter through joins. Closes #176 Audio event library endpoint is still not working. Current filter endpoints: - /bookmarks/filter - /projects/filter - /audio_recordings/filter (joining enabled) - /audio_events/filter (joining enabled) - /audio_event_comments/filter - /sites/filter (joining enabled) --- .../audio_event_comments_controller.rb | 5 +- app/controllers/audio_events_controller.rb | 5 +- app/models/site.rb | 17 +- config/routes.rb | 10 +- lib/modules/filter/build.rb | 101 ++++++----- lib/modules/filter/query.rb | 10 +- lib/modules/filter/validate.rb | 6 +- spec/acceptance/audio_event_comments_spec.rb | 4 +- spec/acceptance/audio_events_spec.rb | 8 +- spec/acceptance/sites_spec.rb | 2 +- spec/lib/filter/query_spec.rb | 160 ++++++++++++++---- .../audio_event_comments_routing_spec.rb | 6 +- spec/routing/audio_events_routing_spec.rb | 3 + 13 files changed, 233 insertions(+), 104 deletions(-) diff --git a/app/controllers/audio_event_comments_controller.rb b/app/controllers/audio_event_comments_controller.rb index f9fd505d..f127609d 100644 --- a/app/controllers/audio_event_comments_controller.rb +++ b/app/controllers/audio_event_comments_controller.rb @@ -2,12 +2,12 @@ class AudioEventCommentsController < ApplicationController include Api::ControllerHelper # order matters for before_action and load_and_authorize_resource! - load_and_authorize_resource :audio_event + load_and_authorize_resource :audio_event, except: [:filter] # this is necessary so that the ability has access to permission.project before_action :build_audio_event_comment, only: [:new, :create] - load_and_authorize_resource :audio_event_comment, through: :audio_event, through_association: :comments + load_and_authorize_resource :audio_event_comment, through: :audio_event, through_association: :comments, except: [:filter] # GET /audio_event_comments # GET /audio_event_comments.json @@ -82,6 +82,7 @@ def destroy end def filter + authorize! :filter, AudioEventComment filter_response = Settings.api_response.response_filter( api_filter_params, get_audio_event_comments, diff --git a/app/controllers/audio_events_controller.rb b/app/controllers/audio_events_controller.rb index 6ddff9db..95819573 100644 --- a/app/controllers/audio_events_controller.rb +++ b/app/controllers/audio_events_controller.rb @@ -3,8 +3,8 @@ class AudioEventsController < ApplicationController include Api::ControllerHelper - load_and_authorize_resource :audio_recording, except: [:show, :library, :library_paged, :download] - load_and_authorize_resource :audio_event, through: :audio_recording, except: [:show, :library, :library_paged, :download] + load_and_authorize_resource :audio_recording, except: [:show, :library, :library_paged, :download, :filter] + load_and_authorize_resource :audio_event, through: :audio_recording, except: [:show, :library, :library_paged, :download, :filter] skip_authorization_check only: [:show, :library, :library_paged] # GET /audio_events @@ -105,6 +105,7 @@ def destroy end def filter + authorize! :filter, AudioEvent filter_response = Settings.api_response.response_filter( api_filter_params, Access::Query.audio_events(current_user, Access::Core.levels_allow), diff --git a/app/models/site.rb b/app/models/site.rb index 07e4fee6..9a7f9830 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -172,7 +172,22 @@ def self.filter_settings defaults: { order_by: :name, direction: :asc - } + }, + valid_associations: [ + { + join: Arel::Table.new(:projects_sites), + on: Site.arel_table[:id].eq(Arel::Table.new(:projects_sites)[:site_id]), + available: false, + associations: [ + { + join: Project, + on: Arel::Table.new(:projects_sites)[:project_id].eq(Project.arel_table[:id]), + available: true + } + ] + + } + ] } end diff --git a/config/routes.rb b/config/routes.rb index 5b29b221..8bf53d36 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -213,15 +213,13 @@ # placed above related resource so it does not conflict with (resource)/:id => (resource)#show match 'audio_recordings/filter' => 'audio_recordings#filter', via: [:get, :post], defaults: {format: 'json'} - + match 'audio_events/filter' => 'audio_events#filter', via: [:get, :post], defaults: {format: 'json'} # API audio recording item resources :audio_recordings, only: [:index, :show, :new, :update], defaults: {format: 'json'} do match 'media.:format' => 'media#show', defaults: {format: 'json'}, as: :media, via: [:get, :head] match 'analysis.:format' => 'analysis#show', defaults: {format: 'json'}, as: :analysis, via: [:get, :head] - match 'audio_events/filter' => 'audio_events#filter', via: [:get, :post], defaults: {format: 'json'} - resources :audio_events, except: [:edit], defaults: {format: 'json'} do collection do get 'download', defaults: {format: 'csv'} @@ -241,10 +239,12 @@ # API tags resources :tags, only: [:index, :show, :create, :new], defaults: {format: 'json'} + # placed above related resource so it does not conflict with (resource)/:id => (resource)#show + match 'audio_event_comments/filter' => 'audio_event_comments#filter', via: [:get, :post], defaults: {format: 'json'} + # API audio_event create resources :audio_events, only: [], defaults: {format: 'json'} do - # placed above related resource so it does not conflict with (resource)/:id => (resource)#show - match 'comments/filter' => 'audio_event_comments#filter', via: [:get, :post], defaults: {format: 'json'} + resources :audio_event_comments, except: [:edit], defaults: {format: 'json'}, path: :comments, as: :comments collection do get 'library' diff --git a/lib/modules/filter/build.rb b/lib/modules/filter/build.rb index 3d5b09af..234a6cb1 100644 --- a/lib/modules/filter/build.rb +++ b/lib/modules/filter/build.rb @@ -158,30 +158,21 @@ def generic_equals(filter_hash) end - # Parse a filter hash. + # Parse a filter. # @param [Hash] filter_hash # @return [Hash] def parse(filter_hash) - conditions, joins = parse_filter(filter_hash) - - # using .where in Rails will do 'AND' by default - #final_conditions = combiner_one(:and, conditions) - - # add joins to final_conditions - join_sql = [] - joins.each do |j| - left_outer_join = AudioRecording.arel_table.join(j[:join].arel_table, Arel::Nodes::OuterJoin).on(j[:on]) - sources = left_outer_join.join_sources - fail CustomErrors::FilterArgumentError.new("SQL contained more than one join: #{sources}") if sources.size != 1 - join_sql.push(sources[0].to_sql) - end - - [conditions, join_sql] + parse_filter(filter_hash) end private - def parse_filter(primary, secondary = nil, extra = nil, joins = []) + # Parse a filter hash. + # @param [Hash, Symbol] primary + # @param [Hash, Object] secondary + # @param [nil, Hash] extra + # @return [Arel::Nodes::Node, Array] + def parse_filter(primary, secondary = nil, extra = nil) if primary.is_a?(Hash) fail CustomErrors::FilterArgumentError.new("Filter hash must have at least 1 entry, got #{primary.size}.", {hash: primary}) if primary.blank? || primary.size < 1 @@ -190,15 +181,15 @@ def parse_filter(primary, secondary = nil, extra = nil, joins = []) conditions = [] primary.each do |key, value| - condition, joins = parse_filter(key, value, secondary, joins) - if condition.is_a?(Array) - conditions.push(*condition) + result = parse_filter(key, value, secondary) + if result.is_a?(Array) + conditions.push(*result) else - conditions.push(condition) + conditions.push(result) end end - [conditions, joins] + conditions elsif primary.is_a?(Symbol) @@ -206,26 +197,33 @@ def parse_filter(primary, secondary = nil, extra = nil, joins = []) when :and, :or combiner = primary filter_hash = secondary - condition, joins = parse_filter(filter_hash, nil, nil, joins) - [combiner_one(combiner, condition), joins] + result = parse_filter(filter_hash) + combiner_one(combiner, result) when :not #combiner = primary filter_hash = secondary - condition, joins = parse_filter(filter_hash, nil, nil, joins) - if condition.respond_to?(:map) - negated_conditions = condition.map { |c| compose_not(c) } + #fail CustomErrors::FilterArgumentError.new("'Not' must have a single combiner or field name, got #{filter_hash.size}", {hash: filter_hash}) if filter_hash.size != 1 + + result = parse_filter(filter_hash) + + #fail CustomErrors::FilterArgumentError.new("'Not' must have a single filter, got #{hash.size}.", {hash: filter_hash}) if result.size != 1 + + if result.respond_to?(:map) + negated_conditions = result.map { |c| compose_not(c) } else - negated_conditions = [compose_not(condition)] + negated_conditions = [compose_not(result)] end + negated_conditions - [negated_conditions, joins] when *@valid_fields.dup.push(/\./) field = primary field_conditions = secondary info = parse_table_field(@table, field, @filter_settings) - condition, joins = parse_filter(field_conditions, info, nil, joins) - [condition, joins] + result = parse_filter(field_conditions, info) + + build_subquery(info, result) + when *@valid_conditions filter_name = primary filter_value = secondary @@ -233,17 +231,9 @@ def parse_filter(primary, secondary = nil, extra = nil, joins = []) table = info[:arel_table] column_name = info[:field_name] - model = info[:model] valid_fields = info[:filter_settings][:valid_fields] - # add join table to joins array if necessary - additional_joins, match = build_joins(model, @valid_associations) - - current_models = joins.map { |j| j[:join]} - new_joins = additional_joins.select { |j| !current_models.include?(j[:join]) } - joins.push(*new_joins) - - [condition(filter_name, table, column_name, valid_fields, filter_value), joins] + condition(filter_name, table, column_name, valid_fields, filter_value) else fail CustomErrors::FilterArgumentError.new("Unrecognised combiner or field name: #{primary}.") end @@ -314,6 +304,37 @@ def condition(filter_name, table, column_name, valid_fields, filter_value) end end + def build_subquery(info, conditions) + + current_table = info[:arel_table] + model = info[:model] + + if current_table != @table + subquery = @table.project(@table[:id]) + + # add conditions to subquery + if conditions.respond_to?(:each) + conditions.each { |c| subquery = subquery.where(c) } + else + subquery = subquery.where(result) + end + + # add relevant joins + joins, match = build_joins(model, @valid_associations) + + joins.each do |j| + table = j[:join] + # assume this is an arel_table if it doesn't respond to .arel_table + arel_table = table.respond_to?(:arel_table) ? table.arel_table : table + subquery = subquery.join(arel_table, Arel::Nodes::OuterJoin).on(j[:on]) + end + + compose_in(@table, :id, [:id], subquery) + else + conditions + end + + end # Build table field from field symbol. # @param [Arel::Table] table diff --git a/lib/modules/filter/query.rb b/lib/modules/filter/query.rb index f0c189ee..836f53d9 100644 --- a/lib/modules/filter/query.rb +++ b/lib/modules/filter/query.rb @@ -133,14 +133,8 @@ def query_without_filter_paging_sorting # @return [ActiveRecord::Relation] query def query_filter(query) if has_filter_params? - conditions, joins = @build.parse(@filter) - - # add distinct if there are joins - query = query.distinct if joins.size > 0 - - query = apply_conditions(query, conditions) - query = apply_joins(query, joins) if joins.size > 0 - query + conditions = @build.parse(@filter) + apply_conditions(query, conditions) else query end diff --git a/lib/modules/filter/validate.rb b/lib/modules/filter/validate.rb index 1cb1fb37..64332583 100644 --- a/lib/modules/filter/validate.rb +++ b/lib/modules/filter/validate.rb @@ -261,8 +261,8 @@ def validate_filter_settings(value) validate_array(value[:render_fields]) validate_array_items(value[:render_fields]) - validate_array(value[:text_fields]) - validate_array_items(value[:text_fields]) + validate_array(value[:text_fields]) unless value[:text_fields].blank? + validate_array_items(value[:text_fields]) unless value[:text_fields].blank? fail CustomErrors::FilterArgumentError, 'Controller name must be a symbol.' unless value[:controller].is_a?(Symbol) fail CustomErrors::FilterArgumentError, 'Action name must be a symbol.' unless value[:action].is_a?(Symbol) @@ -272,7 +272,7 @@ def validate_filter_settings(value) fail CustomErrors::FilterArgumentError, 'Order by must be a symbol.' unless value[:defaults][:order_by].is_a?(Symbol) fail CustomErrors::FilterArgumentError, 'Direction must be a symbol.' unless value[:defaults][:direction].is_a?(Symbol) - validate_array(value[:valid_associations]) + #validate_array(value[:valid_associations]) end diff --git a/spec/acceptance/audio_event_comments_spec.rb b/spec/acceptance/audio_event_comments_spec.rb index 3e9b3932..0813c5bd 100644 --- a/spec/acceptance/audio_event_comments_spec.rb +++ b/spec/acceptance/audio_event_comments_spec.rb @@ -458,10 +458,8 @@ # Filter ##################### - post '/audio_events/:audio_event_id/comments/filter' do - parameter :audio_event_id, 'Requested audio event id (in path/route)', required: true + post '/audio_event_comments/filter' do let(:authentication_token) { reader_token } - let(:audio_event_id) { @comment_user.audio_event_id } let(:raw_post) { { 'filter' => { 'comment' => { diff --git a/spec/acceptance/audio_events_spec.rb b/spec/acceptance/audio_events_spec.rb index a06060b7..e2ff94dc 100644 --- a/spec/acceptance/audio_events_spec.rb +++ b/spec/acceptance/audio_events_spec.rb @@ -2041,9 +2041,7 @@ def library_request(settings = {}) # Filter ##################### - post '/audio_recordings/:audio_recording_id/audio_events/filter' do - parameter :audio_recording_id, 'Requested audio recording id (in path/route)', required: true - + post '/audio_events/filter' do let(:authentication_token) { reader_token } let(:raw_post) { { 'filter' => { @@ -2065,9 +2063,7 @@ def library_request(settings = {}) }) end - post '/audio_recordings/:audio_recording_id/audio_events/filter' do - parameter :audio_recording_id, 'Requested audio recording id (in path/route)', required: true - + post '/audio_events/filter' do let(:authentication_token) { reader_token } let(:raw_post) { { 'filter' => { diff --git a/spec/acceptance/sites_spec.rb b/spec/acceptance/sites_spec.rb index e69e6d49..21be979c 100644 --- a/spec/acceptance/sites_spec.rb +++ b/spec/acceptance/sites_spec.rb @@ -320,7 +320,7 @@ let(:authentication_token) { writer_token } let(:raw_post) { { 'filter' => { - 'projectIds' => { + 'projects.id' => { 'in' => [@write_permission.project.id.to_s] } } diff --git a/spec/lib/filter/query_spec.rb b/spec/lib/filter/query_spec.rb index 804c72dd..643adb32 100644 --- a/spec/lib/filter/query_spec.rb +++ b/spec/lib/filter/query_spec.rb @@ -35,7 +35,7 @@ def create_filter(params) } } ).query_full - }.to raise_error(CustomErrors::FilterArgumentError, /Unrecognised filter not_a_real_filter/) + }.to raise_error(CustomErrors::FilterArgumentError, 'Unrecognised combiner or field name: not_a_real_filter.') end it 'occurs when or has only 1 entry' do @@ -64,7 +64,7 @@ def create_filter(params) } } ).query_full - }.to raise_error(CustomErrors::FilterArgumentError, /Conditions hash must have at least 1 entry, got 0/) + }.to raise_error(CustomErrors::FilterArgumentError, 'Filter hash must have at least 1 entry, got 0.') end it 'occurs when or has no entries' do @@ -77,7 +77,7 @@ def create_filter(params) } } ).query_full - }.to raise_error(CustomErrors::FilterArgumentError, /Conditions hash must have at least 1 entry, got 0/) + }.to raise_error(CustomErrors::FilterArgumentError, /Filter hash must have at least 1 entry, got 0/) end it 'occurs when not has more than one field' do @@ -96,7 +96,7 @@ def create_filter(params) } } ).query_full - }.to raise_error(CustomErrors::FilterArgumentError, /'Not' must have a single combiner or field name, got 2/) + }.to_not raise_error end it 'occurs when not has more than one filter' do @@ -113,7 +113,7 @@ def create_filter(params) } } ).query_full - }.to raise_error(CustomErrors::FilterArgumentError, /'Not' must have a single filter, got 2/) + }.to_not raise_error end it 'occurs when a combiner is not recognised with valid filters' do @@ -221,7 +221,7 @@ def create_filter(params) } } ).query_full - }.to raise_error(CustomErrors::FilterArgumentError, /Conditions hash must have at least 1 entry, got 0/) + }.to raise_error(CustomErrors::FilterArgumentError, /Filter hash must have at least 1 entry, got 0/) end it 'occurs when projection includes invalid field' do @@ -435,6 +435,9 @@ def create_filter(params) } } }, + 'audio_events.is_reference' => { + eq: true + }, or: { recorded_date: { contains: 'Hello' @@ -453,11 +456,17 @@ def create_filter(params) channels: { eq: 1, less_than_or_equal: 8888 + }, + 'sites.id' => { + eq: 5 } }, not: { duration_seconds: { not_eq: 140 + }, + 'tags.text' => { + contains: 'koala' } } }, @@ -485,7 +494,8 @@ def create_filter(params) complex_result = "SELECT\"audio_recordings\".\"recorded_date\",\"audio_recordings\".\"site_id\", \ \"audio_recordings\".\"duration_seconds\",\"audio_recordings\".\"media_type\" \ -FROM\"audio_recordings\"WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ +FROM\"audio_recordings\" \ +WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ AND(\"audio_recordings\".\"site_id\"<123456 \ AND\"audio_recordings\".\"site_id\">9876 \ AND\"audio_recordings\".\"site_id\"IN(1,2,3) \ @@ -499,7 +509,12 @@ def create_filter(params) AND\"audio_recordings\".\"status\"<='128' \ AND(\"audio_recordings\".\"duration_seconds\"!=40 \ ORNOT(\"audio_recordings\".\"channels\"<=9999))) \ -AND(((((((\"audio_recordings\".\"recorded_date\"ILIKE'%Hello%' \ +AND\"audio_recordings\".\"id\"IN( \ +SELECT\"audio_recordings\".\"id\" \ +FROM\"audio_recordings\" \ +LEFTOUTERJOIN\"audio_events\"ON\"audio_recordings\".\"id\"=\"audio_events\".\"audio_recording_id\" \ +WHERE\"audio_events\".\"is_reference\"='t') \ +AND((((((((\"audio_recordings\".\"recorded_date\"ILIKE'%Hello%' \ OR\"audio_recordings\".\"media_type\"ILIKE'%world') \ OR\"audio_recordings\".\"duration_seconds\"=60) \ OR\"audio_recordings\".\"duration_seconds\"<=70) \ @@ -507,12 +522,27 @@ def create_filter(params) OR\"audio_recordings\".\"duration_seconds\">=80) \ OR\"audio_recordings\".\"channels\"=1) \ OR\"audio_recordings\".\"channels\"<=8888) \ -AND(NOT(\"audio_recordings\".\"duration_seconds\"!=140)) \ +OR\"audio_recordings\".\"id\"IN( \ +SELECT\"audio_recordings\".\"id\" \ +FROM\"audio_recordings\" \ +LEFTOUTERJOIN\"sites\"ON\"audio_recordings\".\"site_id\"=\"sites\".\"id\" \ +WHERE\"sites\".\"id\"=5)) \ +AND( \ +NOT(\"audio_recordings\".\"duration_seconds\"!=140)) \ +AND( \ +NOT(\"audio_recordings\".\"id\"IN( \ +SELECT\"audio_recordings\".\"id\" \ +FROM\"audio_recordings\" \ +LEFTOUTERJOIN\"audio_events\"ON\"audio_recordings\".\"id\"=\"audio_events\".\"audio_recording_id\" \ +LEFTOUTERJOIN\"audio_events_tags\"ON\"audio_events\".\"id\"=\"audio_events_tags\".\"audio_event_id\" \ +LEFTOUTERJOIN\"tags\"ON\"audio_events_tags\".\"tag_id\"=\"tags\".\"id\" \ +WHERE\"tags\".\"text\"ILIKE'%koala%'))) \ AND(\"audio_recordings\".\"media_type\"ILIKE'%testing\\_testing%' \ OR\"audio_recordings\".\"status\"ILIKE'%testing\\_testing%') \ AND(\"audio_recordings\".\"status\"='hello' \ AND\"audio_recordings\".\"channels\"=28) \ -ORDERBY\"audio_recordings\".\"duration_seconds\"DESCLIMIT10OFFSET0" +ORDERBY\"audio_recordings\".\"duration_seconds\"DESC \ +LIMIT10OFFSET0" compare_filter_sql(complex_sample, complex_result) @@ -522,14 +552,23 @@ def create_filter(params) complex_result_2 = "SELECT\"audio_recordings\".\"recorded_date\",\"audio_recordings\".\"site_id\", \ - \"audio_recordings\".\"duration_seconds\",\"audio_recordings\".\"media_type\" \ +\"audio_recordings\".\"duration_seconds\",\"audio_recordings\".\"media_type\" \ FROM\"audio_recordings\" \ -INNERJOIN\"sites\"ON\"sites\".\"id\"=\"audio_recordings\".\"site_id\"AND(\"sites\".\"deleted_at\"ISNULL) \ +INNERJOIN\"sites\"ON\"sites\".\"id\"=\"audio_recordings\".\"site_id\" \ +AND(\"sites\".\"deleted_at\"ISNULL) \ INNERJOIN\"projects_sites\"ON\"projects_sites\".\"site_id\"=\"sites\".\"id\" \ -INNERJOIN\"projects\"ON\"projects\".\"id\"=\"projects_sites\".\"project_id\"AND(\"projects\".\"deleted_at\"ISNULL) \ +INNERJOIN\"projects\"ON\"projects\".\"id\"=\"projects_sites\".\"project_id\" \ +AND(\"projects\".\"deleted_at\"ISNULL) \ WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ -AND(\"projects\".\"id\"IN(SELECT\"projects\".\"id\"FROM\"projects\"WHERE(\"projects\".\"deleted_at\"ISNULL)AND\"projects\".\"creator_id\"=#{user_id}) \ -OR\"projects\".\"id\"IN(SELECT\"permissions\".\"project_id\"FROM\"permissions\"WHERE\"permissions\".\"user_id\"=#{user_id}AND\"permissions\".\"level\"IN('reader','writer','owner'))) \ +AND(\"projects\".\"id\"IN( \ +SELECT\"projects\".\"id\" \ +FROM\"projects\" \ +WHERE(\"projects\".\"deleted_at\"ISNULL) \ +AND\"projects\".\"creator_id\"=#{user_id})OR\"projects\".\"id\"IN( \ +SELECT\"permissions\".\"project_id\" \ +FROM\"permissions\" \ +WHERE\"permissions\".\"user_id\"=#{user_id} \ +AND\"permissions\".\"level\"IN('reader','writer','owner'))) \ AND(\"audio_recordings\".\"site_id\"<123456 \ AND\"audio_recordings\".\"site_id\">9876 \ AND\"audio_recordings\".\"site_id\"IN(1,2,3) \ @@ -542,21 +581,40 @@ def create_filter(params) AND\"audio_recordings\".\"status\">='123' \ AND\"audio_recordings\".\"status\"<='128' \ AND(\"audio_recordings\".\"duration_seconds\"!=40 \ -ORNOT(\"audio_recordings\".\"channels\"<=9999))) \ -AND(((((((\"audio_recordings\".\"recorded_date\"ILIKE'%Hello%' \ +OR \ +NOT(\"audio_recordings\".\"channels\"<=9999))) \ +AND\"audio_recordings\".\"id\"IN( \ +SELECT\"audio_recordings\".\"id\" \ +FROM\"audio_recordings\" \ +LEFTOUTERJOIN\"audio_events\"ON\"audio_recordings\".\"id\"=\"audio_events\".\"audio_recording_id\" \ +WHERE\"audio_events\".\"is_reference\"='t') \ +AND((((((((\"audio_recordings\".\"recorded_date\"ILIKE'%Hello%' \ OR\"audio_recordings\".\"media_type\"ILIKE'%world') \ OR\"audio_recordings\".\"duration_seconds\"=60) \ -OR\"audio_recordings\".\"duration_seconds\"<=70) \ -OR\"audio_recordings\".\"duration_seconds\"=50) \ +OR\"audio_recordings\".\"duration_seconds\"<=70)OR\"audio_recordings\".\"duration_seconds\"=50) \ OR\"audio_recordings\".\"duration_seconds\">=80) \ OR\"audio_recordings\".\"channels\"=1) \ OR\"audio_recordings\".\"channels\"<=8888) \ -AND(NOT(\"audio_recordings\".\"duration_seconds\"!=140)) \ +OR\"audio_recordings\".\"id\"IN( \ +SELECT\"audio_recordings\".\"id\"FROM\"audio_recordings\" \ +LEFTOUTERJOIN\"sites\"ON\"audio_recordings\".\"site_id\"=\"sites\".\"id\" \ +WHERE\"sites\".\"id\"=5)) \ +AND( \ +NOT(\"audio_recordings\".\"duration_seconds\"!=140)) \ +AND( \ +NOT(\"audio_recordings\".\"id\"IN( \ +SELECT\"audio_recordings\".\"id\" \ +FROM\"audio_recordings\" \ +LEFTOUTERJOIN\"audio_events\"ON\"audio_recordings\".\"id\"=\"audio_events\".\"audio_recording_id\" \ +LEFTOUTERJOIN\"audio_events_tags\"ON\"audio_events\".\"id\"=\"audio_events_tags\".\"audio_event_id\" \ +LEFTOUTERJOIN\"tags\"ON\"audio_events_tags\".\"tag_id\"=\"tags\".\"id\" \ +WHERE\"tags\".\"text\"ILIKE'%koala%'))) \ AND(\"audio_recordings\".\"media_type\"ILIKE'%testing\\_testing%' \ OR\"audio_recordings\".\"status\"ILIKE'%testing\\_testing%') \ AND(\"audio_recordings\".\"status\"='hello' \ AND\"audio_recordings\".\"channels\"=28) \ -ORDERBY\"audio_recordings\".\"duration_seconds\"DESCLIMIT10OFFSET0" +ORDERBY\"audio_recordings\".\"duration_seconds\"DESC \ +LIMIT10OFFSET0" filter_query = Filter::Query.new( @@ -595,17 +653,29 @@ def create_filter(params) } } complex_result = - "SELECTDISTINCT\"audio_recordings\".\"id\",\"audio_recordings\".\"duration_seconds\" \ + "SELECT\"audio_recordings\".\"id\",\"audio_recordings\".\"duration_seconds\" \ +FROM\"audio_recordings\" \ +WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ +AND\"audio_recordings\".\"id\"IN( \ +SELECT\"audio_recordings\".\"id\" \ +FROM\"audio_recordings\" \ +LEFTOUTERJOIN\"sites\"ON\"audio_recordings\".\"site_id\"=\"sites\".\"id\" \ +WHERE\"sites\".\"id\"=5) \ +AND\"audio_recordings\".\"id\"IN( \ +SELECT\"audio_recordings\".\"id\" \ +FROM\"audio_recordings\" \ +LEFTOUTERJOIN\"audio_events\"ON\"audio_recordings\".\"id\"=\"audio_events\".\"audio_recording_id\" \ +WHERE\"audio_events\".\"is_reference\"='t') \ +AND\"audio_recordings\".\"id\"IN( \ +SELECT\"audio_recordings\".\"id\" \ FROM\"audio_recordings\" \ LEFTOUTERJOIN\"audio_events\"ON\"audio_recordings\".\"id\"=\"audio_events\".\"audio_recording_id\" \ LEFTOUTERJOIN\"audio_events_tags\"ON\"audio_events\".\"id\"=\"audio_events_tags\".\"audio_event_id\" \ LEFTOUTERJOIN\"tags\"ON\"audio_events_tags\".\"tag_id\"=\"tags\".\"id\" \ -WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ -AND\"sites\".\"id\"=5 \ -AND\"audio_events\".\"is_reference\"='t' \ -AND(\"tags\".\"text\"ILIKE'%koala%') \ +WHERE\"tags\".\"text\"ILIKE'%koala%') \ ORDERBY\"audio_recordings\".\"recorded_date\"DESC \ LIMIT25OFFSET0" + compare_filter_sql(request_body_obj, complex_result) @permission = FactoryGirl.create(:write_permission) @@ -613,12 +683,42 @@ def create_filter(params) user_id = user.id complex_result_2 = - "SELECT\"audio_recordings\".\"id\", \ - \"audio_recordings\".\"duration_seconds\" \ + "SELECT\"audio_recordings\".\"id\",\"audio_recordings\".\"duration_seconds\" \ FROM\"audio_recordings\" \ +INNERJOIN\"sites\"ON\"sites\".\"id\"=\"audio_recordings\".\"site_id\" \ +AND(\"sites\".\"deleted_at\"ISNULL) \ +INNERJOIN\"projects_sites\"ON\"projects_sites\".\"site_id\"=\"sites\".\"id\" \ +INNERJOIN\"projects\"ON\"projects\".\"id\"=\"projects_sites\".\"project_id\" \ +AND(\"projects\".\"deleted_at\"ISNULL) \ WHERE(\"audio_recordings\".\"deleted_at\"ISNULL) \ -AND\"audio_recordings\".\"site_id\"=5 \ -ORDERBY\"audio_recordings\".\"recorded_date\"DESCLIMIT25OFFSET0" +AND(\"projects\".\"id\"IN( \ +SELECT\"projects\".\"id\" \ +FROM\"projects\" \ +WHERE(\"projects\".\"deleted_at\"ISNULL) \ +AND\"projects\".\"creator_id\"=#{user_id})OR\"projects\".\"id\"IN( \ +SELECT\"permissions\".\"project_id\" \ +FROM\"permissions\" \ +WHERE\"permissions\".\"user_id\"=#{user_id} \ +AND\"permissions\".\"level\"IN('reader','writer','owner'))) \ +AND\"audio_recordings\".\"id\"IN( \ +SELECT\"audio_recordings\".\"id\"FROM\"audio_recordings\" \ +LEFTOUTERJOIN\"sites\"ON\"audio_recordings\".\"site_id\"=\"sites\".\"id\" \ +WHERE\"sites\".\"id\"=5) \ +AND\"audio_recordings\".\"id\"IN( \ +SELECT\"audio_recordings\".\"id\" \ +FROM\"audio_recordings\" \ +LEFTOUTERJOIN\"audio_events\"ON\"audio_recordings\".\"id\"=\"audio_events\".\"audio_recording_id\" \ +WHERE\"audio_events\".\"is_reference\"='t') \ +AND\"audio_recordings\".\"id\"IN( \ +SELECT\"audio_recordings\".\"id\" \ +FROM\"audio_recordings\" \ +LEFTOUTERJOIN\"audio_events\"ON\"audio_recordings\".\"id\"=\"audio_events\".\"audio_recording_id\" \ +LEFTOUTERJOIN\"audio_events_tags\"ON\"audio_events\".\"id\"=\"audio_events_tags\".\"audio_event_id\" \ +LEFTOUTERJOIN\"tags\"ON\"audio_events_tags\".\"tag_id\"=\"tags\".\"id\" \ +WHERE\"tags\".\"text\"ILIKE'%koala%') \ +ORDERBY\"audio_recordings\".\"recorded_date\"DESC \ +LIMIT25OFFSET0" + filter_query = Filter::Query.new( request_body_obj, diff --git a/spec/routing/audio_event_comments_routing_spec.rb b/spec/routing/audio_event_comments_routing_spec.rb index 77ed6ff6..d173b2f0 100644 --- a/spec/routing/audio_event_comments_routing_spec.rb +++ b/spec/routing/audio_event_comments_routing_spec.rb @@ -16,11 +16,11 @@ it { expect(get('/audio_event_comments')).to route_to('errors#route_error', requested_route: 'audio_event_comments') } - it { expect(get('/audio_events/1/comments/filter')).to route_to('audio_event_comments#filter',audio_event_id: '1', format: 'json') } - it { expect(post('/audio_events/1/comments/filter')).to route_to('audio_event_comments#filter', audio_event_id: '1', format: 'json') } - # used by client it { expect(get('/audio_events/1/comments/2')).to route_to('audio_event_comments#show', audio_event_id: '1', id: '2', format: 'json') } + it { expect(get('/audio_event_comments/filter')).to route_to('audio_event_comments#filter', format: 'json') } + it { expect(post('/audio_event_comments/filter')).to route_to('audio_event_comments#filter', format: 'json') } + end end diff --git a/spec/routing/audio_events_routing_spec.rb b/spec/routing/audio_events_routing_spec.rb index dc708c50..90883540 100644 --- a/spec/routing/audio_events_routing_spec.rb +++ b/spec/routing/audio_events_routing_spec.rb @@ -30,5 +30,8 @@ it { expect(get('/audio_events/library')).to route_to('audio_events#library', format: 'json') } it { expect(get('/audio_events/library/paged')).to route_to('audio_events#library_paged', format: 'json') } + + it { expect(get('/audio_events/filter')).to route_to('audio_events#filter',format: 'json') } + it { expect(post('/audio_events/filter')).to route_to('audio_events#filter', format: 'json') } end end \ No newline at end of file From 7593725447da15b272eb88fcbe83e016d7e5972b Mon Sep 17 00:00:00 2001 From: cofiem Date: Tue, 17 Mar 2015 22:31:32 +1000 Subject: [PATCH 40/49] added filter endpoint for tags. Updated changelog. --- CHANGELOG.md | 7 ++++ app/controllers/tags_controller.rb | 48 ++++++++++++++-------- app/models/ability.rb | 2 +- config/routes.rb | 4 ++ spec/acceptance/tags_spec.rb | 66 ++++++++++++++---------------- spec/routing/tags_routing_spec.rb | 3 ++ 6 files changed, 77 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80c20dba..9a43a70a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased + - 2015-03-17 + - Enhancement: filter api now supports filtering by neighbouring models [#176](https://github.com/QutBioacoustics/baw-server/issues/176) + + - 2015-03-16 + - Regression fixed: paging link format [#169](https://github.com/QutBioacoustics/baw-server/issues/169) + - Regression fixed: Projections options now respected [#170](https://github.com/QutBioacoustics/baw-server/issues/170) + - 2015-03-13 - Small UI bug fixes - Added links to play audio and visualise projects and sites [#164](https://github.com/QutBioacoustics/baw-server/issues/164) [#172](https://github.com/QutBioacoustics/baw-server/issues/172) diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 867b9ce3..9b34aaba 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -1,43 +1,59 @@ class TagsController < ApplicationController + include Api::ControllerHelper - load_and_authorize_resource :tag - respond_to :json + load_and_authorize_resource # GET /tags.json # GET /projects/1/sites/1/audio_recordings/1/audio_events/1/tags.json def index - if params[:audio_event_id] - @audio_event = AudioEvent.where(id: params[:audio_event_id]).first - respond_with @audio_event.tags - elsif params[:filter] #single tag, partial match - respond_with Tag.where("text ILIKE '%?%'", params[:filter]).limit(20) - else - respond_with Tag.all + + respond_to do |format| + format.json { + if params[:audio_event_id] + @audio_event = AudioEvent.where(id: params[:audio_event_id]).first + render json: @audio_event.tags.to_json(only: Tag.filter_settings[:render_fields]) + elsif params[:filter] #single tag, partial match + render json: Tag.where("text ILIKE '%?%'", params[:filter]).limit(20).to_json(only: Tag.filter_settings[:render_fields]) + else + render json: Tag.all.to_json(only: Tag.filter_settings[:render_fields]) + end + } end + end - # GET /tags/1 # GET /tags/1.json def show - respond_with @tag + respond_show end - # GET /tags/new # GET /tags/new.json def new - render json: @tag.to_json(except: [:created_at, :creator_id, :updated_at, :updater_id] ) + respond_show end - # POST /tags # POST /tags.json def create if @tag.save - render json: @tag, status: :created + respond_create_success else - render json: @tag.errors, status: :unprocessable_entity + respond_change_fail end end + # POST /sites/filter.json + # GET /sites/filter.json + def filter + filter_response = Settings.api_response.response_filter( + api_filter_params, + Tag.all, + Tag, + Tag.filter_settings + ) + + render_api_response(filter_response) + end + private def tag_params diff --git a/app/models/ability.rb b/app/models/ability.rb index 811d70f5..df9f8a38 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -166,7 +166,7 @@ def initialize(user) can [:index, :new, :filter], Bookmark # anyone can create tags - can [:index, :new, :create, :show], Tag + can [:index, :new, :create, :show, :filter], Tag elsif Access::Check.is_harvester?(user) # harvester user is used by baw-harvester and baw-workers diff --git a/config/routes.rb b/config/routes.rb index 8bf53d36..f1b08c9c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -236,6 +236,10 @@ end end + + # placed above related resource so it does not conflict with (resource)/:id => (resource)#show + match 'tags/filter' => 'tags#filter', via: [:get, :post], defaults: {format: 'json'} + # API tags resources :tags, only: [:index, :show, :create, :new], defaults: {format: 'json'} diff --git a/spec/acceptance/tags_spec.rb b/spec/acceptance/tags_spec.rb index a2934ed8..c4f4d816 100644 --- a/spec/acceptance/tags_spec.rb +++ b/spec/acceptance/tags_spec.rb @@ -40,38 +40,32 @@ ################################ # LIST ################################ - # get '/projects/:project_id/sites/:site_id/audio_recordings/:audio_recording_id/audio_events/:audio_event_id/tags' do - # # Execute request with ids defined in above let(:id) statements - # parameter :project_id, 'Accessed project ID (in path/route)', required: true - # parameter :site_id, 'Accessed site ID (in path/route)', required: true - # parameter :audio_recording_id, 'Requested audio recording ID (in path/route)', required: true - # parameter :audio_event_id, 'Requested audio event ID (in path/route)', required: true - # - # let(:authentication_token) { writer_token} - # standard_request('LIST for audio_event (as writer)', 200, '0/is_taxanomic', true) - # end - # - # get '/projects/:project_id/sites/:site_id/audio_recordings/:audio_recording_id/audio_events/:audio_event_id/tags' do - # # Execute request with ids defined in above let(:id) statements - # parameter :project_id, 'Accessed project ID (in path/route)', required: true - # parameter :site_id, 'Accessed site ID (in path/route)', required: true - # parameter :audio_recording_id, 'Requested audio recording ID (in path/route)', required: true - # parameter :audio_event_id, 'Requested audio event ID (in path/route)', required: true - # - # let(:authentication_token) { reader_token} - # standard_request('LIST for audio_event (as reader)', 200, '0/is_taxanomic', true) - # end - # - # get '/projects/:project_id/sites/:site_id/audio_recordings/:audio_recording_id/audio_events/:audio_event_id/tags' do - # # Execute request with ids defined in above let(:id) statements - # parameter :project_id, 'Accessed project ID (in path/route)', required: true - # parameter :site_id, 'Accessed site ID (in path/route)', required: true - # parameter :audio_recording_id, 'Requested audio recording ID (in path/route)', required: true - # parameter :audio_event_id, 'Requested audio event ID (in path/route)', required: true - # - # let(:authentication_token) { unconfirmed_token} - # standard_request('LIST for audio_event (as unconfirmed user)', 403, nil, true) - # end + get '/audio_recordings/:audio_recording_id/audio_events/:audio_event_id/tags' do + # Execute request with ids defined in above let(:id) statements + parameter :audio_recording_id, 'Requested audio recording ID (in path/route)', required: true + parameter :audio_event_id, 'Requested audio event ID (in path/route)', required: true + + let(:authentication_token) { writer_token} + standard_request_options(:get, 'LIST for audio_event (as writer)', :ok, {expected_json_path: '0/is_taxanomic'}) + end + + get '/audio_recordings/:audio_recording_id/audio_events/:audio_event_id/tags' do + # Execute request with ids defined in above let(:id) statements + parameter :audio_recording_id, 'Requested audio recording ID (in path/route)', required: true + parameter :audio_event_id, 'Requested audio event ID (in path/route)', required: true + + let(:authentication_token) { reader_token} + standard_request_options(:get, 'LIST for audio_event (as reader)', :ok, {expected_json_path: '0/is_taxanomic'}) + end + + get '/audio_recordings/:audio_recording_id/audio_events/:audio_event_id/tags' do + # Execute request with ids defined in above let(:id) statements + parameter :audio_recording_id, 'Requested audio recording ID (in path/route)', required: true + parameter :audio_event_id, 'Requested audio event ID (in path/route)', required: true + + let(:authentication_token) { unconfirmed_token} + standard_request_options(:get, 'LIST for audio_event (as unconfirmed user)', :forbidden, {expected_json_path: 'meta/error/links/confirm your account'}) + end get '/tags' do let(:authentication_token) { confirmed_token} @@ -97,14 +91,14 @@ parameter :id, 'Requested tag ID (in path/route)', required: true let(:authentication_token) { writer_token} - standard_request('SHOW (as writer)', 200, 'is_taxanomic', true) + standard_request_options(:get, 'SHOW (as writer)', :ok, {expected_json_path: 'data/is_taxanomic'}) #, 'is_taxanomic', true) end get '/tags/:id' do parameter :id, 'Requested tag ID (in path/route)', required: true let(:authentication_token) { reader_token} - standard_request('SHOW (as reader)', 200, 'is_taxanomic', true) + standard_request_options(:get, 'SHOW (as reader)', :ok, {expected_json_path: 'data/is_taxanomic'}) #, 'is_taxanomic', true) end ################################ @@ -120,7 +114,7 @@ let(:raw_post) { {'tag' => post_attributes}.to_json } let(:authentication_token) { writer_token} - standard_request('CREATE (as writer)', 201, 'is_taxanomic', true) + standard_request_options(:post, 'CREATE (as writer)', :created, {expected_json_path: 'data/is_taxanomic'}) end @@ -135,7 +129,7 @@ let(:authentication_token) { unconfirmed_token} # TODO: check what the result should be - standard_request('CREATE (as unconfirmed user)', 403, nil, true) + standard_request_options(:post, 'CREATE (as unconfirmed user)', :forbidden, {expected_json_path: 'meta/error/links/confirm your account'}) end end \ No newline at end of file diff --git a/spec/routing/tags_routing_spec.rb b/spec/routing/tags_routing_spec.rb index adf36477..b5371647 100644 --- a/spec/routing/tags_routing_spec.rb +++ b/spec/routing/tags_routing_spec.rb @@ -19,5 +19,8 @@ it { expect(get('/tags')).to route_to('tags#index', format: 'json') } it { expect(get('/tags/1')).to route_to('tags#show', id: '1', format: 'json') } + it { expect(get('/tags/filter')).to route_to('tags#filter', format: 'json') } + it { expect(post('/tags/filter')).to route_to('tags#filter', format: 'json') } + end end \ No newline at end of file From 02da8350bf5fb04dde8b365a06d6d6939c7d7392 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sun, 22 Mar 2015 20:56:08 +1000 Subject: [PATCH 41/49] removed audio event library endpoints #128 --- app/controllers/audio_events_controller.rb | 60 - config/routes.rb | 12 +- spec/acceptance/audio_events_spec.rb | 1485 -------------------- 3 files changed, 4 insertions(+), 1553 deletions(-) diff --git a/app/controllers/audio_events_controller.rb b/app/controllers/audio_events_controller.rb index 95819573..829b3b14 100644 --- a/app/controllers/audio_events_controller.rb +++ b/app/controllers/audio_events_controller.rb @@ -20,32 +20,6 @@ def index end end - def library - authorize! :library, AudioEvent - audio_event_library_params_cleaned = CleanParams.perform(audio_event_library_params) - response_hash = library_format(current_user, audio_event_library_params_cleaned) - render json: response_hash - end - - def library_paged - authorize! :library, AudioEvent - audio_event_library_params_cleaned = CleanParams.perform(audio_event_library_params) - response_hash = library_format(current_user, audio_event_library_params_cleaned) - - total_query = AudioEvent.filtered(current_user, audio_event_library_params_cleaned).offset(nil).limit(nil) - total = total_query.distinct(false).uniq(false).except(:select).count - - paged_info = { - page: audio_event_library_params_cleaned[:page], - items: audio_event_library_params_cleaned[:items], - total: total, - entries: response_hash - } - - render json: paged_info - - end - # GET /audio_events/1 # GET /audio_events/1.json def show @@ -242,24 +216,6 @@ def download_format(audio_events) list end - # @param [User] current_user - # @param [Hash] request_params - def library_format(current_user, request_params) - request_params[:page] = AudioEvent.filter_count(request_params, :page, 1, 1) - request_params[:items] = AudioEvent.filter_count(request_params, :items, 10, 1, 30) - - query = AudioEvent.filtered(current_user, request_params) - - response_hash = [] - - query.map do |audio_event| - audio_event_hash = json_format(audio_event) - response_hash.push(audio_event_hash) - end - - response_hash - end - # @param [AudioEvent] audio_event def json_format(audio_event) @@ -345,22 +301,6 @@ def audio_event_download_params :format) end - def audio_event_library_params - params.permit( - :reference, - :tagsPartial, :tags_partial, - :audio_recording_id, :audioRecordingId, :audiorecording_id, :audiorecordingId, :recording_id, :recordingId, - :freqMin, :freq_min, - :freqMax, :freq_max, - :annotationDuration, :annotation_duration, - :userId, :user_id, - :page, - :items, - :format, - audio_event: {} - ) - end - def audio_event_show_params params.permit(:id, :project_id, :site_id, :format, :audio_recording_id, audio_event: {}) end diff --git a/config/routes.rb b/config/routes.rb index f1b08c9c..4c84b0f2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -140,7 +140,6 @@ # list: "/audio_recordings/{recordingId}/audio_events", # show: "/audio_recordings/{recordingId}/audio_events/{audioEventId}", # csv: "/audio_recordings/{recordingId}/audio_events/download.{format}", - # library: "/audio_events/library/paged" # }, # tagging: { # list: "/audio_recordings/{recordingId}/audio_events/{audioEventId}/taggings", @@ -250,10 +249,6 @@ resources :audio_events, only: [], defaults: {format: 'json'} do resources :audio_event_comments, except: [:edit], defaults: {format: 'json'}, path: :comments, as: :comments - collection do - get 'library' - get 'library/paged' => 'audio_events#library_paged', as: :library_paged - end end # custom routes for scripts @@ -320,10 +315,11 @@ # exceptions testing route - only available in test env if ENV['RAILS_ENV'] == 'test' - match '/test_exceptions', to: 'errors#test_exceptions', via: :all + # via: :all seems to not work any more >:( + match '/test_exceptions', to: 'errors#test_exceptions', via: [:get, :head, :post, :put, :delete, :options, :trace, :patch] end - # for error pages (add via: :all for rails 4) - match '*requested_route', to: 'errors#route_error', via: :all + # for error pages + match '*requested_route', to: 'errors#route_error', via: [:get, :head, :post, :put, :delete, :options, :trace, :patch] end diff --git a/spec/acceptance/audio_events_spec.rb b/spec/acceptance/audio_events_spec.rb index e2ff94dc..44a5938a 100644 --- a/spec/acceptance/audio_events_spec.rb +++ b/spec/acceptance/audio_events_spec.rb @@ -202,1491 +202,6 @@ def library_request(settings = {}) end - ################################ - # LIBRARY - ################################ - get '/audio_events/library' do - let(:authentication_token) { writer_token } - standard_request('LIBRARY (as writer)', 200, '0/start_time_seconds', true) - end - - get '/audio_events/library' do - let(:authentication_token) { reader_token } - standard_request('LIBRARY (as reader)', 200, '0/start_time_seconds', true) - end - - get '/audio_events/library' do - let(:authentication_token) { admin_token } - standard_request('LIBRARY (as admin)', 200, '0/start_time_seconds', true) - end - - get '/audio_events/library' do - let(:authentication_token) { unconfirmed_token } - standard_request('LIBRARY (as unconfirmed user)', 403, nil, true) - end - - get '/audio_events/library/paged' do - let(:authentication_token) { writer_token } - standard_request('LIBRARY (as writer)', 200, 'entries/0/start_time_seconds', true) - end - - get '/audio_events/library/paged' do - let(:authentication_token) { reader_token } - standard_request('LIBRARY (as reader)', 200, 'entries/0/start_time_seconds', true) - end - - get '/audio_events/library/paged' do - let(:authentication_token) { admin_token } - standard_request('LIBRARY (as admin)', 200, 'entries/0/start_time_seconds', true) - end - - get '/audio_events/library/paged' do - let(:authentication_token) { unconfirmed_token } - standard_request('LIBRARY (as unconfirmed user)', 403, nil, true) - end - - ################################ - # LIBRARY FILTERS - ################################ - - get '/audio_events/library' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - let(:ordered_audio_recordings) { [id] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, default annotations)', - expected_status: 200, - expected_json_path: '0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - - user_creator = FactoryGirl.create(:user, id: 5) - - koala = FactoryGirl.create(:tag, text: 'koala', creator: user_creator) - lewin = FactoryGirl.create(:tag, text: 'lewin', creator: user_creator) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - tagging = FactoryGirl.create(:tagging, creator: user_creator, tag: koala, audio_event: ae) - tagging = FactoryGirl.create(:tagging, creator: user_creator, tag: lewin, audio_event: ae) - - end - - # default sort is 'audio_events.created_at DESC' - let(:ordered_audio_recordings) { [9991, id] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, one additional annotation)', - expected_status: 200, - expected_json_path: '1/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library?freqMin=450.3&freqMax=500.2&annotationDuration=0.54' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - - user_creator = FactoryGirl.create(:user, id: 5) - - koala = FactoryGirl.create(:tag, text: 'koala', creator: user_creator) - lewin = FactoryGirl.create(:tag, text: 'lewin', creator: user_creator) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 2, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - tagging = FactoryGirl.create(:tagging, creator: user_creator, tag: koala, audio_event: ae) - tagging = FactoryGirl.create(:tagging, creator: user_creator, tag: lewin, audio_event: ae) - end - - let(:ordered_audio_recordings) { [9991, 9992, id] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, ordered by bounds: duration)', - expected_status: 200, - expected_json_path: '0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library?freqMin=450.3&freqMax=500.2&annotationDuration=0.54' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - user_creator = FactoryGirl.create(:user, id: 5) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 451, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae3 = FactoryGirl.create(:audio_event, - id: 9993, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 445, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - end - - let(:ordered_audio_recordings) { [9991, 9992, 9993, id] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, ordered by bounds: freqMin)', - expected_status: 200, - expected_json_path: '0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library?freqMin=450.3&freqMax=500.2&annotationDuration=0.54' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - user_creator = FactoryGirl.create(:user, id: 5) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500, - is_reference: true, - creator: user_creator) - - ae3 = FactoryGirl.create(:audio_event, - id: 9993, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 510, - is_reference: true, - creator: user_creator) - end - - let(:ordered_audio_recordings) { [9991, 9992, 9993, id] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, ordered by bounds: freqMax)', - expected_status: 200, - expected_json_path: '0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library?userId=99998' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - user_creator = FactoryGirl.create(:user, id: 99998) - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - user_creator2 = FactoryGirl.create(:user, id: 99997) - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500, - is_reference: true, - creator: user_creator2) - - end - - let(:ordered_audio_recordings) { [9991] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, filter by userId)', - expected_status: 200, - expected_json_path: '0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library?reference=true' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - user_creator = FactoryGirl.create(:user, id: 5) - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500, - is_reference: false, - creator: user_creator) - - end - - let(:ordered_audio_recordings) { [9991] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, filter by reference)', - expected_status: 200, - expected_json_path: '0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library?tagsPartial=koala,lewi' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - - user_creator = FactoryGirl.create(:user, id: 99999) - - koala = FactoryGirl.create(:tag, text: 'koala', creator: user_creator) - lewin = FactoryGirl.create(:tag, text: 'lewin', creator: user_creator) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 2, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - FactoryGirl.create(:tagging, creator: user_creator, tag: koala, audio_event: ae) - FactoryGirl.create(:tagging, creator: user_creator, tag: lewin, audio_event: ae2) - end - - # default sort order is audio_events.created_at DESC - let(:ordered_audio_recordings) { [9992, 9991] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, filter by tagsPartial)', - expected_status: 200, - expected_json_path: '0/start_time_seconds', - document: true - }) - end - - get '/audio_events/library?audioRecordingId=9987654' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - - user_creator = FactoryGirl.create(:user, id: 99999) - - existing_audio_recording = @write_permission.project.sites[0].audio_recordings[0] - - audio_recording = FactoryGirl.create(:audio_recording, - id: 9987654, - site: @write_permission.project.sites[0], - recorded_date: Time.zone.parse('2001-07-21 00:00:00')) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: audio_recording, - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: existing_audio_recording, - start_time_seconds: 1, - end_time_seconds: 2, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - end - - # default sort order is audio_events.created_at DESC - let(:ordered_audio_recordings) { [9991] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, filter by audioRecordingId)', - expected_status: 200, - expected_json_path: '0/start_time_seconds', - document: true - }) - end - - get '/audio_events/library?reference=true&tagsPartial=ewi,ala&audioRecordingId=99876&freqMin=450.3&freqMax=500.2&annotationDuration=0.54&userId=9998&page=1&items=10' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - - user_creator = FactoryGirl.create(:user, id: 9998) - lewin = FactoryGirl.create(:tag, text: 'lewin', creator: user_creator) - koala = FactoryGirl.create(:tag, text: 'koala', creator: user_creator) - - audio_recording = FactoryGirl.create(:audio_recording, - id: 99876, - site: @write_permission.project.sites[0], - recorded_date: Time.zone.parse('2001-08-21 00:00:00')) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: audio_recording, - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: audio_recording, - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 505, - is_reference: true, - creator: user_creator) - - FactoryGirl.create(:tagging, creator: user_creator, tag: lewin, audio_event: ae) - FactoryGirl.create(:tagging, creator: user_creator, tag: koala, audio_event: ae2) - end - - let(:ordered_audio_recordings) { [9991, 9992] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, all filters)', - expected_status: 200, - expected_json_path: '0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { other_user_token } - - before do - user_creator = FactoryGirl.create(:user, id: 5) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 451, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae3 = FactoryGirl.create(:audio_event, - id: 9993, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 445, - high_frequency_hertz: 500.2, - is_reference: false, - creator: user_creator) - end - - # default sort order is audio_events.created_at DESC - let(:ordered_audio_recordings) { [9992, 9991] } - - library_request( - { - description: 'LIBRARY (as no project access, able to access all reference audio_events)', - expected_status: 200, - expected_json_path: '0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { unconfirmed_token } - - before do - user_creator = FactoryGirl.create(:user, id: 5) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 451, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae3 = FactoryGirl.create(:audio_event, - id: 9993, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 445, - high_frequency_hertz: 500.2, - is_reference: false, - creator: user_creator) - end - - # default sort order is audio_events.created_at DESC - let(:ordered_audio_recordings) { nil } - - library_request( - { - description: 'LIBRARY (as unconfirmed, prevent access to all audio_events)', - expected_status: 403, - document: true - }) - - end - - get '/audio_events/library' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - #let(:authentication_token) { unconfirmed_token } - - before do - user_creator = FactoryGirl.create(:user, id: 5) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 451, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae3 = FactoryGirl.create(:audio_event, - id: 9993, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 445, - high_frequency_hertz: 500.2, - is_reference: false, - creator: user_creator) - end - - # default sort order is audio_events.created_at DESC - let(:ordered_audio_recordings) { nil } - - library_request( - { - description: 'LIBRARY (as anon, prevent access to all audio_events)', - expected_status: 401, - document: true - }) - - end - - ################################ - # PAGED LIBRARY FILTERS - ################################ - - get '/audio_events/library/paged?reference=true' do - let(:authentication_token) { writer_token } - - before do - FactoryGirl.create(:audio_event, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 23, end_time_seconds: 25, is_reference: true) - end - - standard_request_options(:get, 'LIBRARY PAGED (as writer)', :ok, { - expected_json_path: 'entries/0/audio_recording_duration_seconds' - }) - end - - get '/audio_events/library/paged' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - let(:ordered_audio_recordings) { [id] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, default annotations)', - expected_status: 200, - expected_json_path: 'entries/0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library/paged' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - - user_creator = FactoryGirl.create(:user, id: 5) - - koala = FactoryGirl.create(:tag, text: 'koala', creator: user_creator) - lewin = FactoryGirl.create(:tag, text: 'lewin', creator: user_creator) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - tagging = FactoryGirl.create(:tagging, creator: user_creator, tag: koala, audio_event: ae) - tagging = FactoryGirl.create(:tagging, creator: user_creator, tag: lewin, audio_event: ae) - - end - - # default sort is 'audio_events.created_at DESC' - let(:ordered_audio_recordings) { [9991, id] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, one additional annotation)', - expected_status: 200, - expected_json_path: 'entries/1/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library/paged?freq_min=450.3&freq_max=500.2&annotation_duration=0.54' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - - user_creator = FactoryGirl.create(:user, id: 5) - - koala = FactoryGirl.create(:tag, text: 'koala', creator: user_creator) - lewin = FactoryGirl.create(:tag, text: 'lewin', creator: user_creator) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 2, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - tagging = FactoryGirl.create(:tagging, creator: user_creator, tag: koala, audio_event: ae) - tagging = FactoryGirl.create(:tagging, creator: user_creator, tag: lewin, audio_event: ae) - end - - let(:ordered_audio_recordings) { [9991, 9992, id] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, ordered by bounds: duration)', - expected_status: 200, - expected_json_path: 'entries/0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library/paged?freq_min=450.3&freq_max=500.2&annotation_duration=0.54' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - user_creator = FactoryGirl.create(:user, id: 5) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 451, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae3 = FactoryGirl.create(:audio_event, - id: 9993, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 445, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - end - - let(:ordered_audio_recordings) { [9991, 9992, 9993, id] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, ordered by bounds: freqMin)', - expected_status: 200, - expected_json_path: 'entries/0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library/paged?freq_min=450.3&freq_max=500.2&annotation_duration=0.54' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - user_creator = FactoryGirl.create(:user, id: 5) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500, - is_reference: true, - creator: user_creator) - - ae3 = FactoryGirl.create(:audio_event, - id: 9993, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 510, - is_reference: true, - creator: user_creator) - end - - let(:ordered_audio_recordings) { [9991, 9992, 9993, id] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, ordered by bounds: freqMax)', - expected_status: 200, - expected_json_path: 'entries/0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library/paged?user_id=99998' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - user_creator = FactoryGirl.create(:user, id: 99998) - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - user_creator2 = FactoryGirl.create(:user, id: 99997) - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500, - is_reference: true, - creator: user_creator2) - - end - - let(:ordered_audio_recordings) { [9991] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, filter by userId)', - expected_status: 200, - expected_json_path: 'entries/0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library/paged?reference=true' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - user_creator = FactoryGirl.create(:user, id: 5) - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500, - is_reference: false, - creator: user_creator) - - end - - let(:ordered_audio_recordings) { [9991] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, filter by reference)', - expected_status: 200, - expected_json_path: 'entries/0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library/paged?tags_partial=koala,lewi' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - - user_creator = FactoryGirl.create(:user, id: 99999) - - koala = FactoryGirl.create(:tag, text: 'koala', creator: user_creator) - lewin = FactoryGirl.create(:tag, text: 'lewin', creator: user_creator) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 2, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - FactoryGirl.create(:tagging, creator: user_creator, tag: koala, audio_event: ae) - FactoryGirl.create(:tagging, creator: user_creator, tag: lewin, audio_event: ae2) - end - - # default sort order is audio_events.created_at DESC - let(:ordered_audio_recordings) { [9992, 9991] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, filter by tagsPartial)', - expected_status: 200, - expected_json_path: 'entries/0/start_time_seconds', - document: true - }) - end - - get '/audio_events/library/paged?audio_recording_id=9987654' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - - user_creator = FactoryGirl.create(:user, id: 99999) - - existing_audio_recording = @write_permission.project.sites[0].audio_recordings[0] - - audio_recording = FactoryGirl.create(:audio_recording, - id: 9987654, - site: @write_permission.project.sites[0], - recorded_date: Time.zone.parse('2001-07-21 00:00:00')) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: audio_recording, - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: existing_audio_recording, - start_time_seconds: 1, - end_time_seconds: 2, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - end - - # default sort order is audio_events.created_at DESC - let(:ordered_audio_recordings) { [9991] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, two additional, filter by audioRecordingId)', - expected_status: 200, - expected_json_path: 'entries/0/start_time_seconds', - document: true - }) - end - - get '/audio_events/library/paged?reference=true&tags_partial=ewi,ala&audiorecording_id=99876&freq_min=450.3&freq_max=500.2&annotation_duration=0.54&user_id=9998&page=1&items=10' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { reader_token } - - before do - - user_creator = FactoryGirl.create(:user, id: 9998) - lewin = FactoryGirl.create(:tag, text: 'lewin', creator: user_creator) - koala = FactoryGirl.create(:tag, text: 'koala', creator: user_creator) - - audio_recording = FactoryGirl.create(:audio_recording, - id: 99876, - site: @write_permission.project.sites[0], - recorded_date: Time.zone.parse('2001-08-21 00:00:00')) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: audio_recording, - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: audio_recording, - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 505, - is_reference: true, - creator: user_creator) - - FactoryGirl.create(:tagging, creator: user_creator, tag: lewin, audio_event: ae) - FactoryGirl.create(:tagging, creator: user_creator, tag: koala, audio_event: ae2) - end - - let(:ordered_audio_recordings) { [9991, 9992] } - - library_request( - { - description: 'LIBRARY (as reader with parameters, all filters)', - expected_status: 200, - expected_json_path: 'entries/0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library/paged' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { other_user_token } - - before do - user_creator = FactoryGirl.create(:user, id: 5) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 451, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae3 = FactoryGirl.create(:audio_event, - id: 9993, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 445, - high_frequency_hertz: 500.2, - is_reference: false, - creator: user_creator) - end - - # default sort order is audio_events.created_at DESC - let(:ordered_audio_recordings) { [9992, 9991] } - - library_request( - { - description: 'LIBRARY (as no project access, able to access all reference audio_events)', - expected_status: 200, - expected_json_path: 'entries/0/start_time_seconds', - document: true - }) - - end - - get '/audio_events/library/paged' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - let(:authentication_token) { unconfirmed_token } - - before do - user_creator = FactoryGirl.create(:user, id: 5) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 451, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae3 = FactoryGirl.create(:audio_event, - id: 9993, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 445, - high_frequency_hertz: 500.2, - is_reference: false, - creator: user_creator) - end - - # default sort order is audio_events.created_at DESC - let(:ordered_audio_recordings) { nil } - - library_request( - { - description: 'LIBRARY (as unconfirmed, prevent access to all audio_events)', - expected_status: 403, - document: true - }) - - end - - get '/audio_events/library/paged' do - - parameter :reference, '[true, false] (optional)' - parameter :tagsPartial, 'comma separated text (optional)' - parameter :freqMin, 'double (optional)' - parameter :freqMax, 'double (optional)' - parameter :annotationDuration, 'double (optional)' - parameter :page, 'int (optional)' - parameter :items, 'int (optional)' - parameter :userId, 'int (optional)' - parameter :audioRecordingId, 'int (optional)' - - #let(:authentication_token) { unconfirmed_token } - - before do - user_creator = FactoryGirl.create(:user, id: 5) - - ae = FactoryGirl.create(:audio_event, - id: 9991, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 450.3, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae2 = FactoryGirl.create(:audio_event, - id: 9992, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 451, - high_frequency_hertz: 500.2, - is_reference: true, - creator: user_creator) - - ae3 = FactoryGirl.create(:audio_event, - id: 9993, - audio_recording: @write_permission.project.sites[0].audio_recordings[0], - start_time_seconds: 1, - end_time_seconds: 1.54, - low_frequency_hertz: 445, - high_frequency_hertz: 500.2, - is_reference: false, - creator: user_creator) - end - - # default sort order is audio_events.created_at DESC - let(:ordered_audio_recordings) { nil } - - library_request( - { - description: 'LIBRARY (as anon, prevent access to all audio_events)', - expected_status: 401, - document: true - }) - - end - ################################ # SHOW ################################ From 719d7443c9e2ad79ebe19635b1c0727a7729d555 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sun, 22 Mar 2015 20:57:07 +1000 Subject: [PATCH 42/49] updated routes after removing audio event library endpoint #128 --- spec/routing/audio_events_routing_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/routing/audio_events_routing_spec.rb b/spec/routing/audio_events_routing_spec.rb index 90883540..f0d218e5 100644 --- a/spec/routing/audio_events_routing_spec.rb +++ b/spec/routing/audio_events_routing_spec.rb @@ -28,8 +28,8 @@ it { expect(get('/audio_recordings/3/audio_events/download')).to route_to('audio_events#download', audio_recording_id: '3', format: 'csv') } it { expect(get('/audio_recordings/3/audio_events/download.csv')).to route_to('audio_events#download', audio_recording_id: '3', format: 'csv') } - it { expect(get('/audio_events/library')).to route_to('audio_events#library', format: 'json') } - it { expect(get('/audio_events/library/paged')).to route_to('audio_events#library_paged', format: 'json') } + it { expect(get('/audio_events/library')).to route_to('errors#route_error', requested_route: 'audio_events/library') } + it { expect(get('/audio_events/library/paged')).to route_to('errors#route_error', requested_route: 'audio_events/library/paged') } it { expect(get('/audio_events/filter')).to route_to('audio_events#filter',format: 'json') } it { expect(post('/audio_events/filter')).to route_to('audio_events#filter', format: 'json') } From 1b6692837f600482921ba939c36c6f17b79ed930 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sun, 22 Mar 2015 20:57:33 +1000 Subject: [PATCH 43/49] Added possibility for filter to restrict by calculated fields. Includes removing model methods for audio event library #128 --- app/models/audio_event.rb | 164 ++-------------------------- lib/modules/filter/build.rb | 91 +++++++++++++++- lib/modules/filter/comparison.rb | 98 +++++++++++++++-- lib/modules/filter/subset.rb | 177 +++++++++++++++++++++++++++---- lib/modules/filter/validate.rb | 5 + spec/lib/filter/query_spec.rb | 64 ++++++++++- 6 files changed, 416 insertions(+), 183 deletions(-) diff --git a/app/models/audio_event.rb b/app/models/audio_event.rb index bcfe0a2c..896e5356 100644 --- a/app/models/audio_event.rb +++ b/app/models/audio_event.rb @@ -52,7 +52,8 @@ def self.filter_settings :start_time_seconds, :end_time_seconds, :low_frequency_hertz, :high_frequency_hertz, :is_reference, - :created_at, :creator_id, :updated_at], + :created_at, :creator_id, :updated_at, + :duration_seconds], render_fields: [:id, :audio_recording_id, :start_time_seconds, :end_time_seconds, :low_frequency_hertz, :high_frequency_hertz, @@ -65,6 +66,12 @@ def self.filter_settings order_by: :created_at, direction: :desc }, + field_mappings: [ + { + name: :duration_seconds, + value: (AudioEvent.arel_table[:end_time_seconds] - AudioEvent.arel_table[:start_time_seconds]) + } + ], valid_associations: [ { join: AudioRecording, @@ -93,161 +100,6 @@ def self.filter_settings } end - # @param [User] user - # @param [Hash] params - def self.filtered(user, params) - # get a paged collection of all audio_events the current user can access - ### option params ### - # reference: [true, false] (optional) - # tagsPartial: comma separated text (optional) - # freqMin: double (optional) - # freqMax: double (optional) - # annotationDuration: double (optional) - # page: int (optional) - # items: int (optional) - # userId: int (optional) - # audioRecordingId: int (optional) - - query = Access::Query.audio_events(user, Access::Core.levels_allow).joins(:creator, :tags) - - query = AudioEvent.filter_reference(query, params) - query = AudioEvent.filter_tags(query, params) - query = AudioEvent.filter_distance(query, params) - query = AudioEvent.filter_user(query, params) - query = AudioEvent.filter_audio_recording(query, params) - query = AudioEvent.filter_paging(query, params) - - query = query.select('"audio_events".*, "audio_recordings"."recorded_date", "sites"."name", "sites"."id", "users"."user_name", "users"."id"') - Rails.logger.info "AudioEvent filtered: #{query.to_sql}" - query - end - - # @param [ActiveRecord::Relation] query - # @param [Hash] params - def self.filter_tags(query, params) - if params.include?(:tags_partial) && !params[:tags_partial].blank? - tags_partial = CSV.parse(params[:tags_partial], col_sep: ',').flatten.map { |item| item.trim(' ', '') }.join('|').downcase - tags_query = AudioEvent.joins(:tags).where('lower(tags.text) SIMILAR TO ?', "%(#{tags_partial})%").select('audio_events.id') - query.where(id: tags_query) - else - query - end - end - - # @param [ActiveRecord::Relation] query - # @param [Hash] params - def self.filter_reference(query, params) - if params.include?(:reference) && params[:reference] == 'true' - query.where(is_reference: true) - elsif params.include?(:reference) && params[:reference] == 'false' - query.where(is_reference: false) - else - query - end - end - - # @param [Hash] params - # @param [Symbol] params_symbol - # @param [Integer] min - # @param [Integer] max - def self.filter_count(params, params_symbol, default = 1, min = 1, max = nil) - value = default - if params.include?(params_symbol) - value = params[params_symbol].to_i - end - - if value < min - value = min - end - - if !max.blank? && value > max - value = max - end - - value - end - - # Postgres-specific queries - # @param [ActiveRecord::Relation] query - # @param [Hash] params - def self.filter_distance(query, params) - if params.include?(:freq_min) || params.include?(:freq_max) || params.include?(:annotation_duration) - compare_items = [] - compare_text = [] - - if params.include?(:freq_min) - compare_items.push(params[:freq_min].to_f) - compare_text.push('power(audio_events.low_frequency_hertz - ?, 2)') - end - - if params.include?(:freq_max) - compare_items.push(params[:freq_max].to_f) - compare_text.push('power(audio_events.high_frequency_hertz - ?, 2)') - end - - if params.include?(:annotation_duration) - compare_items.push(params[:annotation_duration].to_f) - compare_text.push('power((audio_events.end_time_seconds - audio_events.start_time_seconds) - ?, 2)') - end - - dangerous_sql = 'sqrt('+compare_text.join(' + ')+')' - sanitized_sql = sanitize_sql([dangerous_sql, compare_items].flatten, self.table_name) - query.select(sanitized_sql + ' as distance_calc').order(sanitized_sql) - else - query.order('audio_events.created_at DESC') - end - end - - # @param [ActiveRecord::Relation] query - # @param [Hash] params - def self.filter_paging(query, params) - - defaults = AudioEvent.filter_paging_defaults - - page = defaults[:page] - if params.include?(:page) - page = params[:page].to_i - end - - items = defaults[:items] - if params.include?(:items) - items = params[:items].to_i - end - - query.offset((page - 1) * items).limit(items) - end - - # @param [ActiveRecord::Relation] query - # @param [Hash] params - def self.filter_user(query, params) - if params.include?(:user_id) - creator_id_check = 'audio_events.creator_id = ?' - updater_id_check = 'audio_events.updater_id = ?' - user_id = params[:user_id].to_i - query.where("(#{creator_id_check} OR #{updater_id_check})", user_id, user_id) - else - query - end - end - - # @param [ActiveRecord::Relation] query - # @param [Hash] params - def self.filter_audio_recording(query, params) - if params.include?(:audio_recording_id) || params.include?(:audio_recording_id) || params.include?(:audiorecording_id) - audio_recording_id = (params[:audio_recording_id] || params[:audio_recording_id] || params[:audiorecording_id]).to_i - query.where(audio_recording_id: audio_recording_id) - else - query - end - end - - def self.filter_paging_defaults - { - page: 1, - items: 20 - } - end - def self.csv_filter(user, filter_params) query = Access::Query.audio_events(user, :reader).joins(:creator, :tags) diff --git a/lib/modules/filter/build.rb b/lib/modules/filter/build.rb index 234a6cb1..f705138f 100644 --- a/lib/modules/filter/build.rb +++ b/lib/modules/filter/build.rb @@ -20,6 +20,7 @@ def initialize(table, filter_settings) @valid_fields = filter_settings[:valid_fields].map(&:to_sym) @text_fields = filter_settings[:text_fields].map(&:to_sym) @valid_associations = filter_settings[:valid_associations] + @field_mappings = filter_settings[:field_mappings] @valid_conditions = [ # comparison @@ -233,7 +234,14 @@ def parse_filter(primary, secondary = nil, extra = nil) column_name = info[:field_name] valid_fields = info[:filter_settings][:valid_fields] - condition(filter_name, table, column_name, valid_fields, filter_value) + custom_condition = build_custom_condition(column_name) + + if custom_condition.blank? + condition(filter_name, table, column_name, valid_fields, filter_value) + else + condition_node(filter_name, custom_condition, filter_value) + end + else fail CustomErrors::FilterArgumentError.new("Unrecognised combiner or field name: #{primary}.") end @@ -295,8 +303,72 @@ def condition(filter_name, table, column_name, valid_fields, filter_value) compose_ends_with(table, column_name, valid_fields, filter_value) when :not_ends_with, :not_end_with, :does_not_end_with compose_not_ends_with(table, column_name, valid_fields, filter_value) - when :regex + when :regex, :regex_match, :matches compose_regex(table, column_name, valid_fields, filter_value) + when :not_regex, :not_regex_match, :does_not_match, :not_match + compose_not_regex(table, column_name, valid_fields, filter_value) + + # unknown + else + fail CustomErrors::FilterArgumentError.new("Unrecognised filter #{filter_name}.") + end + end + + # Build a condition. + # @param [Symbol] filter_name + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] filter_value + # @return [Arel::Nodes::Node] condition + def condition_node(filter_name, node, filter_value) + case filter_name + + # comparisons + when :eq, :equal + compose_eq_node(node, filter_value) + when :not_eq, :not_equal + compose_not_eq_node(node, filter_value) + when :lt, :less_than + compose_lt_node(node, filter_value) + when :not_lt, :not_less_than + compose_not_lt_node(node, filter_value) + when :gt, :greater_than + compose_gt_node(node, filter_value) + when :not_gt, :not_greater_than + compose_not_gt_node(node, filter_value) + when :lteq, :less_than_or_equal + compose_lteq_node(node, filter_value) + when :not_lteq, :not_less_than_or_equal + compose_not_lteq_node(node, filter_value) + when :gteq, :greater_than_or_equal + compose_gteq_node(node, filter_value) + when :not_gteq, :not_greater_than_or_equal + compose_not_gteq_node(node, filter_value) + + # subsets + when :range, :in_range + compose_range_options_node(node, filter_value) + when :not_range, :not_in_range + compose_not_range_options_node(node, filter_value) + when :in + compose_in_node(node, filter_value) + when :not_in + compose_not_in_node(node, filter_value) + when :contains, :contain + compose_contains_node(node, filter_value) + when :not_contains, :not_contain, :does_not_contain + compose_not_contains_node(node, filter_value) + when :starts_with, :start_with + compose_starts_with_node(node, filter_value) + when :not_starts_with, :not_start_with, :does_not_start_with + compose_not_starts_with_node(node, filter_value) + when :ends_with, :end_with + compose_ends_with_node(node, filter_value) + when :not_ends_with, :not_end_with, :does_not_end_with + compose_not_ends_with_node(node, filter_value) + when :regex, :regex_match, :matches + compose_regex_node(node, filter_value) + when :not_regex, :not_regex_match, :does_not_match, :not_match + compose_not_regex_node(node, filter_value) # unknown else @@ -447,5 +519,20 @@ def build_joins(model, associations, joins = []) [[], false] end + def build_custom_condition(column_name) + + mappings = {} + unless @field_mappings.blank? + @field_mappings.each { |m| mappings[m[:name]] = m[:value] } + end + + value = mappings[column_name] + if mappings.keys.include?(column_name) && !value.blank? + value + else + nil + end + end + end end \ No newline at end of file diff --git a/lib/modules/filter/comparison.rb b/lib/modules/filter/comparison.rb index 1239988d..39e0a259 100644 --- a/lib/modules/filter/comparison.rb +++ b/lib/modules/filter/comparison.rb @@ -17,7 +17,16 @@ module Comparison # @return [Arel::Nodes::Node] condition def compose_eq(table, column_name, allowed, value) validate_table_column(table, column_name, allowed) - table[column_name].eq(value) + compose_eq_node(table[column_name], value) + end + + # Create equals condition. + # @param [Arel::Nodes::Node] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_eq_node(node, value) + validate_node_or_attribute(node) + node.eq(value) end # Create not equals condition. @@ -28,7 +37,16 @@ def compose_eq(table, column_name, allowed, value) # @return [Arel::Nodes::Node] condition def compose_not_eq(table, column_name, allowed, value) validate_table_column(table, column_name, allowed) - table[column_name].not_eq(value) + compose_not_eq_node(table[column_name], value) + end + + # Create not equals condition. + # @param [Arel::Nodes::Node] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_not_eq_node(node, value) + validate_node_or_attribute(node) + node.not_eq(value) end # Create less than condition. @@ -39,7 +57,16 @@ def compose_not_eq(table, column_name, allowed, value) # @return [Arel::Nodes::Node] condition def compose_lt(table, column_name, allowed, value) validate_table_column(table, column_name, allowed) - table[column_name].lt(value) + compose_lt_node(table[column_name], value) + end + + # Create less than condition. + # @param [Arel::Nodes::Node] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_lt_node(node, value) + validate_node_or_attribute(node) + node.lt(value) end # Create not less than condition. @@ -52,6 +79,14 @@ def compose_not_lt(table, column_name, allowed, value) compose_lt(table, column_name, allowed, value).not end + # Create not less than condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_not_lt_node(node, value) + compose_lt_node(node, value).not + end + # Create greater than condition. # @param [Arel::Table] table # @param [Symbol] column_name @@ -60,7 +95,16 @@ def compose_not_lt(table, column_name, allowed, value) # @return [Arel::Nodes::Node] condition def compose_gt(table, column_name, allowed, value) validate_table_column(table, column_name, allowed) - table[column_name].gt(value) + compose_gt_node(table[column_name], value) + end + + # Create greater than condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_gt_node(node, value) + validate_node_or_attribute(node) + node.gt(value) end # Create not greater than condition. @@ -73,6 +117,14 @@ def compose_not_gt(table, column_name, allowed, value) compose_gt(table, column_name, allowed, value).not end + # Create not greater than condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_not_gt_node(node, value) + compose_gt_node(node, value).not + end + # Create less than or equal condition. # @param [Arel::Table] table # @param [Symbol] column_name @@ -81,7 +133,16 @@ def compose_not_gt(table, column_name, allowed, value) # @return [Arel::Nodes::Node] condition def compose_lteq(table, column_name, allowed, value) validate_table_column(table, column_name, allowed) - table[column_name].lteq(value) + compose_lteq_node(table[column_name], value) + end + + # Create less than or equal condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_lteq_node(node, value) + validate_node_or_attribute(node) + node.lteq(value) end # Create not less than or equal condition. @@ -94,6 +155,14 @@ def compose_not_lteq(table, column_name, allowed, value) compose_lteq(table, column_name, allowed, value).not end + # Create not less than or equal condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_not_lteq_node(node, value) + compose_lteq_node(node, value).not + end + # Create greater than or equal condition. # @param [Arel::Table] table # @param [Symbol] column_name @@ -102,7 +171,16 @@ def compose_not_lteq(table, column_name, allowed, value) # @return [Arel::Nodes::Node] condition def compose_gteq(table, column_name, allowed, value) validate_table_column(table, column_name, allowed) - table[column_name].gteq(value) + compose_gteq_node(table[column_name], value) + end + + # Create greater than or equal condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_gteq_node(node, value) + validate_node_or_attribute(node) + node.gteq(value) end # Create not greater than or equal condition. @@ -115,5 +193,13 @@ def compose_not_gteq(table, column_name, allowed, value) compose_gteq(table, column_name, allowed, value).not end + # Create not greater than or equal condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_not_gteq_node(node, value) + compose_gteq_node(node, value).not + end + end end \ No newline at end of file diff --git a/lib/modules/filter/subset.rb b/lib/modules/filter/subset.rb index 1c974809..caf6eb01 100644 --- a/lib/modules/filter/subset.rb +++ b/lib/modules/filter/subset.rb @@ -16,9 +16,18 @@ module Subset # @return [Arel::Nodes::Node] condition def compose_contains(table, column_name, allowed, value) validate_table_column(table, column_name, allowed) + compose_contains_node(table[column_name],value) + end + + # Create contains condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_contains_node(node, value) + validate_node_or_attribute(node) sanitized_value = sanitize_like_value(value) contains_value = "%#{sanitized_value}%" - table[column_name].matches(contains_value) + node.matches(contains_value) end # Create not contains condition. @@ -31,6 +40,14 @@ def compose_not_contains(table, column_name, allowed, value) compose_contains(table, column_name, allowed, value).not end + # Create not contains condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_not_contains_node(node, value) + compose_contains_node(node, value).not + end + # Create starts_with condition. # @param [Arel::Table] table # @param [Symbol] column_name @@ -39,9 +56,18 @@ def compose_not_contains(table, column_name, allowed, value) # @return [Arel::Nodes::Node] condition def compose_starts_with(table, column_name, allowed, value) validate_table_column(table, column_name, allowed) + compose_starts_with_node(table[column_name], value) + end + + # Create starts_with condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_starts_with_node(node, value) + validate_node_or_attribute(node) sanitized_value = sanitize_like_value(value) contains_value = "#{sanitized_value}%" - table[column_name].matches(contains_value) + node.matches(contains_value) end # Create not starts_with condition. @@ -54,6 +80,14 @@ def compose_not_starts_with(table, column_name, allowed, value) compose_starts_with(table, column_name, allowed, value).not end + # Create not starts_with condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_not_starts_with_node(node, value) + compose_starts_with_node(node, value).not + end + # Create ends_with condition. # @param [Arel::Table] table # @param [Symbol] column_name @@ -62,9 +96,18 @@ def compose_not_starts_with(table, column_name, allowed, value) # @return [Arel::Nodes::Node] condition def compose_ends_with(table, column_name, allowed, value) validate_table_column(table, column_name, allowed) + compose_ends_with_node(table[column_name], value) + end + + # Create ends_with condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_ends_with_node(node, value) + validate_node_or_attribute(node) sanitized_value = sanitize_like_value(value) contains_value = "%#{sanitized_value}" - table[column_name].matches(contains_value) + node.matches(contains_value) end # Create not ends_with condition. @@ -77,6 +120,14 @@ def compose_not_ends_with(table, column_name, allowed, value) compose_ends_with(table, column_name, allowed, value).not end + # Create not ends_with condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_not_ends_with_node(node, value) + compose_ends_with_node(node, value).not + end + # Create IN condition. # @param [Arel::Table] table # @param [Symbol] column_name @@ -84,10 +135,19 @@ def compose_not_ends_with(table, column_name, allowed, value) # @param [Array] values # @return [Arel::Nodes::Node] condition def compose_in(table, column_name, allowed, values) - validate_array(values) validate_table_column(table, column_name, allowed) + compose_in_node(table[column_name], values) + end + + # Create IN condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Array] values + # @return [Arel::Nodes::Node] condition + def compose_in_node(node, values) + validate_node_or_attribute(node) + validate_array(values) validate_array_items(values) if values.is_a?(Array) - table[column_name].in(values) + node.in(values) end # Create NOT IN condition. @@ -100,6 +160,14 @@ def compose_not_in(table, column_name, allowed, values) compose_in(table, column_name, allowed, values).not end + # Create NOT IN condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Array] values + # @return [Arel::Nodes::Node] condition + def compose_not_in_node(node, values) + compose_in_node(node, values).not + end + # Create IN condition using range. # @param [Arel::Table] table # @param [Symbol] column_name @@ -107,22 +175,32 @@ def compose_not_in(table, column_name, allowed, values) # @param [Hash] hash # @return [Arel::Nodes::Node] condition def compose_range_options(table, column_name, allowed, hash) + validate_table_column(table, column_name, allowed) + compose_range_options_node(table[column_name], hash) + end + + + # Create IN condition using range. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Hash] hash + # @return [Arel::Nodes::Node] condition + def compose_range_options_node(node, hash) from = hash[:from] to = hash[:to] interval = hash[:interval] if !from.blank? && !to.blank? && !interval.blank? - fail CustomErrors::FilterArgumentError.new("Range filter must use either ('from' and 'to') or ('interval'), not both.", {field: column_name, hash: hash}) + fail CustomErrors::FilterArgumentError.new("Range filter must use either ('from' and 'to') or ('interval'), not both.", {hash: hash}) elsif from.blank? && !to.blank? - fail CustomErrors::FilterArgumentError.new("Range filter missing 'from'.", {field: column_name, hash: hash}) + fail CustomErrors::FilterArgumentError.new("Range filter missing 'from'.", {hash: hash}) elsif !from.blank? && to.blank? - fail CustomErrors::FilterArgumentError.new("Range filter missing 'to'.", {field: column_name, hash: hash}) + fail CustomErrors::FilterArgumentError.new("Range filter missing 'to'.", {hash: hash}) elsif !from.blank? && !to.blank? - compose_range(table, column_name, allowed, from, to) + compose_range_node(node, from, to) elsif !interval.blank? - compose_range_string(table, column_name, allowed, interval) + compose_range_string_node(node, interval) else - fail CustomErrors::FilterArgumentError.new("Range filter was not valid (#{hash})", {field: column_name, hash: hash}) + fail CustomErrors::FilterArgumentError.new("Range filter was not valid (#{hash})", {hash: hash}) end end @@ -136,6 +214,14 @@ def compose_not_range_options(table, column_name, allowed, hash) compose_range_options(table, column_name, allowed, hash).not end + # Create NOT IN condition using range. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Hash] hash + # @return [Arel::Nodes::Node] condition + def compose_not_range_options_node(node, hash) + compose_range_options_node(node, hash).not + end + # Create IN condition using range. # @param [Arel::Table] table # @param [Symbol] column_name @@ -144,6 +230,15 @@ def compose_not_range_options(table, column_name, allowed, hash) # @return [Arel::Nodes::Node] condition def compose_range_string(table, column_name, allowed, range_string) validate_table_column(table, column_name, allowed) + compose_range_string_node(table[column_name], range_string) + end + + # Create IN condition using range. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [String] range_string + # @return [Arel::Nodes::Node] condition + def compose_range_string_node(node, range_string) + validate_node_or_attribute(node) range_regex = /(\[|\()(.*),(.*)(\)|\])/i matches = range_string.match(range_regex) @@ -159,15 +254,15 @@ def compose_range_string(table, column_name, allowed, range_string) # build using gt, lt, gteq, lteq if start_exclude - start_condition = table[column_name].gt(start_value) + start_condition = node.gt(start_value) else - start_condition =table[column_name].gteq(start_value) + start_condition =node.gteq(start_value) end if end_exclude - end_condition = table[column_name].lt(end_value) + end_condition = node.lt(end_value) else - end_condition =table[column_name].lteq(end_value) + end_condition =node.lteq(end_value) end start_condition.and(end_condition) @@ -183,6 +278,14 @@ def compose_not_range_string(table, column_name, allowed, range_string) compose_range_string(table, column_name, allowed, range_string).not end + # Create NOT IN condition using range. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [String] range_string + # @return [Arel::Nodes::Node] condition + def compose_not_range_string_node(node, range_string) + compose_range_string_node(node, range_string).not + end + # Create IN condition using from (inclusive) and to (exclusive). # @param [Arel::Table] table # @param [Symbol] column_name @@ -192,8 +295,18 @@ def compose_not_range_string(table, column_name, allowed, range_string) # @return [Arel::Nodes::Node] condition def compose_range(table, column_name, allowed, from, to) validate_table_column(table, column_name, allowed) + compose_range_node(table[column_name], from, to) + end + + # Create IN condition using from (inclusive) and to (exclusive). + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] from + # @param [Object] to + # @return [Arel::Nodes::Node] condition + def compose_range_node(node, from, to) + validate_node_or_attribute(node) range = Range.new(from, to, true) - table[column_name].in(range) + node.in(range) end # Create NOT IN condition using from (inclusive) and to (exclusive). @@ -207,6 +320,15 @@ def compose_not_range(table, column_name, allowed, from, to) compose_range(table, column_name, allowed, from, to).not end + # Create NOT IN condition using from (inclusive) and to (exclusive). + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] from + # @param [Object] to + # @return [Arel::Nodes::Node] condition + def compose_not_range_node(node, from, to) + compose_range_node(node, from, to).not + end + # Create regular expression condition. # @param [Arel::Table] table # @param [Symbol] column_name @@ -215,7 +337,16 @@ def compose_not_range(table, column_name, allowed, from, to) # @return [Arel::Nodes::Node] condition def compose_regex(table, column_name, allowed, value) validate_table_column(table, column_name, allowed) - Arel::Nodes::Regexp.new(table[column_name], Arel::Nodes.build_quoted(value)) + compose_regex_node(table[column_name], value) + end + + # Create regular expression condition. + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_regex_node(node, value) + validate_node_or_attribute(node) + Arel::Nodes::Regexp.new(node, Arel::Nodes.build_quoted(value)) end # Create negated regular expression condition. @@ -227,7 +358,17 @@ def compose_regex(table, column_name, allowed, value) # @return [Arel::Nodes::Node] condition def compose_not_regex(table, column_name, allowed, value) validate_table_column(table, column_name, allowed) - Arel::Nodes::NotRegexp.new(table[column_name], Arel::Nodes.build_quoted(value)) + compose_not_regex_node(table[column_name], value) + end + + # Create negated regular expression condition. + # Not available just now, maybe in Arel 6? + # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node + # @param [Object] value + # @return [Arel::Nodes::Node] condition + def compose_not_regex_node(node, value) + validate_node_or_attribute(node) + Arel::Nodes::NotRegexp.new(node, Arel::Nodes.build_quoted(value)) end end diff --git a/lib/modules/filter/validate.rb b/lib/modules/filter/validate.rb index 64332583..6ca39b68 100644 --- a/lib/modules/filter/validate.rb +++ b/lib/modules/filter/validate.rb @@ -126,6 +126,11 @@ def validate_projection(projection) fail CustomErrors::FilterArgumentError, "Condition must be Arel::Attributes::Attribute, got #{projection}" unless projection.is_a?(Arel::Attributes::Attribute) end + def validate_node_or_attribute(value) + check = value.is_a?(Arel::Nodes::Node) || value.is_a?(String) || value.is_a?(Arel::Attributes::Attribute) + fail CustomErrors::FilterArgumentError, "Value must be Arel::Nodes::Node or String or Arel::Attributes::Attribute, got #{value}" unless check + end + # Validate name value. # @param [Symbol] name # @param [Array] allowed diff --git a/spec/lib/filter/query_spec.rb b/spec/lib/filter/query_spec.rb index 643adb32..704b1b3d 100644 --- a/spec/lib/filter/query_spec.rb +++ b/spec/lib/filter/query_spec.rb @@ -422,7 +422,8 @@ def create_filter(params) ends_with: 'ends with text', range: { interval: '[123, 128]' - } + }, + }, or: { duration_seconds: { @@ -731,4 +732,65 @@ def create_filter(params) end end + + context 'calculated field' do + it 'works for audio_event.duration_seconds' do + request_body_obj = { + filter: { + duration_seconds: { + gt: 3 + } + } + } + + @permission = FactoryGirl.create(:write_permission) + user = @permission.user + user_id = user.id + + filter_query = Filter::Query.new( + request_body_obj, + Access::Query.audio_events(user, Access::Core.levels_allow), + AudioEvent, + AudioEvent.filter_settings + ) + + expected_sql = + "SELECT\"audio_events\".\"id\",\"audio_events\".\"audio_recording_id\", \ +\"audio_events\".\"start_time_seconds\",\"audio_events\".\"end_time_seconds\", \ + \"audio_events\".\"low_frequency_hertz\",\"audio_events\".\"high_frequency_hertz\", \ + \"audio_events\".\"is_reference\",\"audio_events\".\"creator_id\", \ + \"audio_events\".\"updated_at\",\"audio_events\".\"created_at\" \ +FROM\"audio_events\" \ +INNERJOIN\"audio_recordings\"ON\"audio_recordings\".\"id\"=\"audio_events\".\"audio_recording_id\" \ +AND(\"audio_recordings\".\"deleted_at\"ISNULL) \ +INNERJOIN\"sites\"ON\"sites\".\"id\"=\"audio_recordings\".\"site_id\" \ +AND(\"sites\".\"deleted_at\"ISNULL) \ +INNERJOIN\"projects_sites\"ON\"projects_sites\".\"site_id\"=\"sites\".\"id\" \ +INNERJOIN\"projects\"ON\"projects\".\"id\"=\"projects_sites\".\"project_id\" \ +AND(\"projects\".\"deleted_at\"ISNULL) \ +WHERE(\"audio_events\".\"deleted_at\"ISNULL) \ +AND((\"projects\".\"id\"IN( \ +SELECT\"projects\".\"id\" \ +FROM\"projects\" \ +WHERE(\"projects\".\"deleted_at\"ISNULL) \ +AND\"projects\".\"creator_id\"=#{user_id}) \ +OR\"projects\".\"id\"IN( \ +SELECT\"permissions\".\"project_id\" \ +FROM\"permissions\" \ +WHERE\"permissions\".\"user_id\"=#{user_id} \ +AND\"permissions\".\"level\"IN('reader','writer','owner'))) \ +OR\"audio_events\".\"id\"IN( \ +SELECT\"audio_events\".\"id\" \ +FROM\"audio_events\" \ +WHERE(\"audio_events\".\"deleted_at\"ISNULL) \ +AND\"audio_events\".\"is_reference\"='t')) \ +AND((\"audio_events\".\"end_time_seconds\"-\"audio_events\".\"start_time_seconds\")>3) \ +ORDERBY\"audio_events\".\"created_at\"DESC \ +LIMIT25OFFSET0" + + expect(filter_query.query_full.to_sql.gsub(/\s+/, '')).to eq(expected_sql.gsub(/\s+/, '')) + + end + end + end From 825a6710a227dd0b97d2521f2743a2609441b5c6 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sun, 22 Mar 2015 21:08:48 +1000 Subject: [PATCH 44/49] updated resque-status and newrelic gems; added changes to changelog --- CHANGELOG.md | 4 +++ Gemfile | 4 +-- Gemfile.lock | 78 ++++++++++++++++++++++++------------------------ config/routes.rb | 1 - 4 files changed, 45 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a43a70a..f6f6aa23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased + - 2015-03-22 + - Removed audio event library endpoint [#128](https://github.com/QutBioacoustics/baw-server/issues/128) + - Added ability to filter by calculated fields + - 2015-03-17 - Enhancement: filter api now supports filtering by neighbouring models [#176](https://github.com/QutBioacoustics/baw-server/issues/176) diff --git a/Gemfile b/Gemfile index e012309e..f92891fc 100644 --- a/Gemfile +++ b/Gemfile @@ -115,7 +115,7 @@ require 'rbconfig' # MONITORING # ------------------------------------- gem 'exception_notification', '~> 4.0.1' -gem 'newrelic_rpm', '~> 3.10.0' +gem 'newrelic_rpm', '~> 3.11.0' # Documentation & UI # ------------------------------------- @@ -133,7 +133,7 @@ gem 'rack-rewrite', '~> 1.5.1' # ------------------------------------ gem 'resque', '~> 1.25.2' gem 'resque-job-stats', git: 'https://github.com/echannel/resque-job-stats.git', branch: :master, ref: '8932c036ae' -gem 'resque-status', '~> 0.4.3' +gem 'resque-status', '~> 0.5.0' # set to a specific commit when releasing to master branch gem 'baw-workers', git: 'https://github.com/QutBioacoustics/baw-workers.git', branch: :master, ref: '9b51d7a82a' diff --git a/Gemfile.lock b/Gemfile.lock index 69fe851d..6ed29d07 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -72,36 +72,36 @@ GIT GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.0) - actionpack (= 4.2.0) - actionview (= 4.2.0) - activejob (= 4.2.0) + actionmailer (4.2.1) + actionpack (= 4.2.1) + actionview (= 4.2.1) + activejob (= 4.2.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.0) - actionview (= 4.2.0) - activesupport (= 4.2.0) - rack (~> 1.6.0) + actionpack (4.2.1) + actionview (= 4.2.1) + activesupport (= 4.2.1) + rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.1) - actionview (4.2.0) - activesupport (= 4.2.0) + actionview (4.2.1) + activesupport (= 4.2.1) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.1) - activejob (4.2.0) - activesupport (= 4.2.0) + activejob (4.2.1) + activesupport (= 4.2.1) globalid (>= 0.3.0) - activemodel (4.2.0) - activesupport (= 4.2.0) + activemodel (4.2.1) + activesupport (= 4.2.1) builder (~> 3.1) - activerecord (4.2.0) - activemodel (= 4.2.0) - activesupport (= 4.2.0) + activerecord (4.2.1) + activemodel (= 4.2.1) + activesupport (= 4.2.1) arel (~> 6.0) - activesupport (4.2.0) + activesupport (4.2.1) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -171,7 +171,7 @@ GEM thor (~> 0.19.1) crack (0.4.2) safe_yaml (~> 1.0.0) - daemons (1.2.1) + daemons (1.2.2) database_cleaner (1.3.0) devise (3.4.1) bcrypt (~> 3.0) @@ -235,7 +235,7 @@ GEM nokogiri (~> 1.6.0) ruby_parser (~> 3.5) i18n (0.7.0) - jbuilder (2.2.11) + jbuilder (2.2.12) activesupport (>= 3.0.0, < 5) multi_json (~> 1.2) jc-validates_timeliness (3.1.1) @@ -272,7 +272,7 @@ GEM net-ssh (>= 2.6.5) net-ssh (2.9.2) netrc (0.10.3) - newrelic_rpm (3.10.0.279) + newrelic_rpm (3.11.0.283) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) notiffany (0.0.6) @@ -304,29 +304,29 @@ GEM haml (~> 4.0, >= 4.0.4) json (~> 1.8, >= 1.8.1) sinatra (~> 1.3, >= 1.3.0) - rails (4.2.0) - actionmailer (= 4.2.0) - actionpack (= 4.2.0) - actionview (= 4.2.0) - activejob (= 4.2.0) - activemodel (= 4.2.0) - activerecord (= 4.2.0) - activesupport (= 4.2.0) + rails (4.2.1) + actionmailer (= 4.2.1) + actionpack (= 4.2.1) + actionview (= 4.2.1) + activejob (= 4.2.1) + activemodel (= 4.2.1) + activerecord (= 4.2.1) + activesupport (= 4.2.1) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.0) + railties (= 4.2.1) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.5) + rails-dom-testing (1.0.6) activesupport (>= 4.2.0.beta, < 5.0) nokogiri (~> 1.6.0) rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.2) loofah (~> 2.0) rails-i18n-debug (1.0.1) - railties (4.2.0) - actionpack (= 4.2.0) - activesupport (= 4.2.0) + railties (4.2.1) + actionpack (= 4.2.1) + activesupport (= 4.2.1) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (10.4.2) @@ -348,7 +348,7 @@ GEM redis-namespace (~> 1.3) sinatra (>= 0.9.2) vegas (~> 0.1.2) - resque-status (0.4.3) + resque-status (0.5.0) resque (~> 1.19) resque_solo (0.1.0) resque (~> 1.25.1) @@ -445,12 +445,12 @@ GEM tins (1.3.5) tzinfo (1.2.2) thread_safe (~> 0.1) - tzinfo-data (1.2015.1) + tzinfo-data (1.2015.2) tzinfo (>= 1.0.0) uglifier (2.7.1) execjs (>= 0.3.0) json (>= 1.8.0) - uniform_notifier (1.7.0) + uniform_notifier (1.8.0) uuidtools (2.1.5) vegas (0.1.11) rack (>= 1.0.0) @@ -506,7 +506,7 @@ DEPENDENCIES jquery-rails (~> 4.0.3) json_spec (~> 1.1.4) launchy (~> 2.4.3) - newrelic_rpm (~> 3.10.0) + newrelic_rpm (~> 3.11.0) notiffany (~> 0.0.3) paperclip (~> 4.2.1) pg (~> 0.18.1) @@ -521,7 +521,7 @@ DEPENDENCIES responders (~> 2.0) resque (~> 1.25.2) resque-job-stats! - resque-status (~> 0.4.3) + resque-status (~> 0.5.0) role_model (~> 0.8.1) rspec (~> 3.2.0) rspec-rails (~> 3.2.0) diff --git a/config/routes.rb b/config/routes.rb index 4c84b0f2..4ff331f1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -247,7 +247,6 @@ # API audio_event create resources :audio_events, only: [], defaults: {format: 'json'} do - resources :audio_event_comments, except: [:edit], defaults: {format: 'json'}, path: :comments, as: :comments end From fa2760598f557b736bce7e2ccb98a497f19cf86c Mon Sep 17 00:00:00 2001 From: cofiem Date: Sun, 22 Mar 2015 22:01:22 +1000 Subject: [PATCH 45/49] added test and better error for invalid range filter --- lib/modules/filter/subset.rb | 2 ++ spec/lib/filter/query_spec.rb | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/lib/modules/filter/subset.rb b/lib/modules/filter/subset.rb index caf6eb01..fa64d9d6 100644 --- a/lib/modules/filter/subset.rb +++ b/lib/modules/filter/subset.rb @@ -185,6 +185,8 @@ def compose_range_options(table, column_name, allowed, hash) # @param [Hash] hash # @return [Arel::Nodes::Node] condition def compose_range_options_node(node, hash) + fail CustomErrors::FilterArgumentError.new("Range filter must be {'from': 'value', 'to': 'value'} or {'interval': 'value'} got #{hash}") unless hash.is_a?(Hash) + from = hash[:from] to = hash[:to] interval = hash[:interval] diff --git a/spec/lib/filter/query_spec.rb b/spec/lib/filter/query_spec.rb index 704b1b3d..808cf662 100644 --- a/spec/lib/filter/query_spec.rb +++ b/spec/lib/filter/query_spec.rb @@ -315,6 +315,13 @@ def create_filter(params) }.to raise_error(CustomErrors::FilterArgumentError, 'Array values cannot be hashes.') end + it 'occurs for an invalid range filter' do + filter_params = {"filter"=>{"durationSeconds"=>{"inRange"=>"(5,6)"}}} + expect { + create_filter(filter_params).query_full + }.to raise_error(CustomErrors::FilterArgumentError, "Range filter must be {'from': 'value', 'to': 'value'} or {'interval': 'value'} got (5,6)") + end + end context 'projection' do @@ -791,6 +798,7 @@ def create_filter(params) expect(filter_query.query_full.to_sql.gsub(/\s+/, '')).to eq(expected_sql.gsub(/\s+/, '')) end + end end From b48177cde4a7895cd6e101a89dad3aefb74524b9 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sun, 22 Mar 2015 22:02:10 +1000 Subject: [PATCH 46/49] fixed user thumbnail layout --- app/assets/stylesheets/application.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 56d59c3d..5db78558 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -26,7 +26,7 @@ h3 { } } } -.thumbnail.right-caption > a img { +.thumbnail.right-caption img { float: left; margin-right: 9px; } From 9616fba9896a21a1835d8d229270999efc6226fb Mon Sep 17 00:00:00 2001 From: cofiem Date: Sun, 22 Mar 2015 22:03:14 +1000 Subject: [PATCH 47/49] fixed site recording duration display --- app/models/audio_recording.rb | 2 ++ app/views/sites/show.html.haml | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/models/audio_recording.rb b/app/models/audio_recording.rb index 16ac501e..b34c70bf 100644 --- a/app/models/audio_recording.rb +++ b/app/models/audio_recording.rb @@ -77,6 +77,8 @@ class AudioRecording < ActiveRecord::Base scope :tag_types, lambda { |tag_types| includes(:tags).where('tags.type_of_tag' => tag_types) } scope :tag_text, lambda { |tag_text| includes(:tags).where(Tag.arel_table[:text].matches("%#{tag_text}%")) } + scope :order_by_absolute_end_desc, lambda { order('recorded_date + CAST(duration_seconds || \' seconds\' as interval) DESC')} + # Check if the original file for this audio recording currently exists. def original_file_exists? self.original_file_paths.length > 0 diff --git a/app/views/sites/show.html.haml b/app/views/sites/show.html.haml index 3a51f546..f1c6ecbd 100644 --- a/app/views/sites/show.html.haml +++ b/app/views/sites/show.html.haml @@ -18,10 +18,15 @@ This site does not contain any audio recordings. - else - duration_sum = @site.audio_recordings.sum(:duration_seconds) - - recorded_min = @site.audio_recordings.minimum(:recorded_date) + + - record_min = @site.audio_recordings.order(recorded_date: :asc).first + - recorded_min = record_min.recorded_date - recorded_min = recorded_min.in_time_zone(@site.rails_tz) unless @site.rails_tz.blank? - - recorded_max = @site.audio_recordings.maximum(:recorded_date) + + - record_max = @site.audio_recordings.order('recorded_date + CAST(duration_seconds || \' seconds\' as interval) DESC').first + - recorded_max = record_max.recorded_date.advance(seconds: record_max.duration_seconds) - recorded_max = recorded_max.in_time_zone(@site.rails_tz) unless @site.rails_tz.blank? + - recorded_diff = recorded_max - recorded_min %p This site contains recordings from #{recorded_min.to_formatted_s(:readable_full_without_seconds)} From c04fd149e2ad74aa6272d841ffb78a9da6da06b1 Mon Sep 17 00:00:00 2001 From: cofiem Date: Sun, 22 Mar 2015 22:03:36 +1000 Subject: [PATCH 48/49] added colon separator for timezone --- config/initializers/date_time_formats.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/date_time_formats.rb b/config/initializers/date_time_formats.rb index ac1e6326..6e3818e0 100644 --- a/config/initializers/date_time_formats.rb +++ b/config/initializers/date_time_formats.rb @@ -2,7 +2,7 @@ Time::DATE_FORMATS[:very_long_time] = '%H:%M:%S.%3N' Time::DATE_FORMATS[:readable_very_long_time] = '%H hr %M min %S.%3N sec' Time::DATE_FORMATS[:readable_full] = lambda { |time| time.strftime("%a, #{ActiveSupport::Inflector.ordinalize(time.day)} %b %Y at %H:%M:%S #{time.formatted_offset(false)}") } -Time::DATE_FORMATS[:readable_full_without_seconds] = lambda { |time| time.strftime("%a, #{ActiveSupport::Inflector.ordinalize(time.day)} %b %Y at %H:%M (#{time.formatted_offset(false)})") } +Time::DATE_FORMATS[:readable_full_without_seconds] = lambda { |time| time.strftime("%a, #{ActiveSupport::Inflector.ordinalize(time.day)} %b %Y at %H:%M (#{time.formatted_offset(true)})") } Date::DATE_FORMATS[:month_and_year] = '%B %Y' Date::DATE_FORMATS[:short_ordinal] = lambda { |date| date.strftime("%B #{date.day.ordinalize}") } Time::DATE_FORMATS[:long_year] = '%Y/%m/%d' \ No newline at end of file From 3e259172832eb1eb6b7e9d1cb42d0fabccd8889d Mon Sep 17 00:00:00 2001 From: cofiem Date: Sun, 22 Mar 2015 22:23:51 +1000 Subject: [PATCH 49/49] changed version to 0.14.0 and updated changelog --- CHANGELOG.md | 2 +- app/models/settings.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f6aa23..6543555b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## [Release 0.14.0](https://github.com/QutBioacoustics/baw-server/releases/tag/0.14.0) (2015-03-22) - 2015-03-22 - Removed audio event library endpoint [#128](https://github.com/QutBioacoustics/baw-server/issues/128) diff --git a/app/models/settings.rb b/app/models/settings.rb index 76ca74f9..9e58ca31 100644 --- a/app/models/settings.rb +++ b/app/models/settings.rb @@ -44,8 +44,8 @@ def version_info # see http://nvie.com/posts/a-successful-git-branching-model/ { major: 0, - minor: 13, - patch: 1, + minor: 14, + patch: 0, pre: '', build: '' }