Commit 7c0c09ab authored by Luc Didry's avatar Luc Didry
Browse files

Add tooltips to help users

parent f959c63d
......@@ -9,10 +9,13 @@ locales:
$(XGETTEXT) $(EXTRACTDIR) -o $(POT) 2>/dev/null
push-locales:
zanata-cli push
zanata-cli -q -B push
pull-locales:
zanata-cli pull
zanata-cli -q -B pull
stats-locales:
zanata-cli -q stats
dev:
MOJO_REVERSE_PROXY=1 $(CARTON) morbo $(DOLOMON) --listen http://127.0.0.1:8400 --watch themes/ --watch dolomon.conf --watch lib/
......
......@@ -1247,17 +1247,17 @@ DISTRIBUTIONS
perl 5.008
strict 0
warnings 0
IO-Socket-SSL-2.054
pathname: S/SU/SULLR/IO-Socket-SSL-2.054.tar.gz
IO-Socket-SSL-2.056
pathname: S/SU/SULLR/IO-Socket-SSL-2.056.tar.gz
provides:
IO::Socket::SSL 2.054
IO::Socket::SSL::Intercept 2.014
IO::Socket::SSL::OCSP_Cache 2.054
IO::Socket::SSL::OCSP_Resolver 2.054
IO::Socket::SSL 2.056
IO::Socket::SSL::Intercept 2.056
IO::Socket::SSL::OCSP_Cache 2.056
IO::Socket::SSL::OCSP_Resolver 2.056
IO::Socket::SSL::PublicSuffix undef
IO::Socket::SSL::SSL_Context 2.054
IO::Socket::SSL::SSL_HANDLE 2.054
IO::Socket::SSL::Session_Cache 2.054
IO::Socket::SSL::SSL_Context 2.056
IO::Socket::SSL::SSL_HANDLE 2.056
IO::Socket::SSL::Session_Cache 2.056
IO::Socket::SSL::Utils 2.014
requirements:
ExtUtils::MakeMaker 0
......@@ -1411,13 +1411,13 @@ DISTRIBUTIONS
Math::Round 0.07
requirements:
ExtUtils::MakeMaker 0
Minion-8.09
pathname: S/SR/SRI/Minion-8.09.tar.gz
Minion-8.10
pathname: S/SR/SRI/Minion-8.10.tar.gz
provides:
LinkCheck undef
LinkCheck::Controller::Links undef
LinkCheck::Task::CheckLinks undef
Minion 8.09
Minion 8.10
Minion::Backend undef
Minion::Backend::Pg undef
Minion::Command::minion undef
......@@ -1425,7 +1425,7 @@ DISTRIBUTIONS
Minion::Command::minion::worker undef
Minion::Job undef
Minion::Worker undef
Minion::_Guard 8.09
Minion::_Guard 8.10
Mojolicious::Plugin::Minion undef
Mojolicious::Plugin::Minion::Admin undef
requirements:
......@@ -1544,8 +1544,8 @@ DISTRIBUTIONS
Mojolicious 7.53
SQL::Abstract 1.85
perl 5.010001
Mojolicious-7.65
pathname: S/SR/SRI/Mojolicious-7.65.tar.gz
Mojolicious-7.69
pathname: S/SR/SRI/Mojolicious-7.69.tar.gz
provides:
Mojo undef
Mojo::Asset undef
......@@ -1614,7 +1614,7 @@ DISTRIBUTIONS
Mojo::UserAgent::Transactor undef
Mojo::Util undef
Mojo::WebSocket undef
Mojolicious 7.65
Mojolicious 7.69
Mojolicious::Command undef
Mojolicious::Command::cgi undef
Mojolicious::Command::cpanify undef
......
......@@ -227,7 +227,9 @@ sub startup {
before_dispatch => sub {
my $c = shift;
$c->res->headers->header('Access-Control-Allow-Origin' => '*');
$c->minion->enqueue('clean_stats');
if ($c->app->time_to_clean) {
$c->minion->enqueue('clean_stats');
}
}
);
......@@ -236,34 +238,28 @@ sub startup {
clean_stats => sub {
my $job = shift;
my $c = $job->app;
my $file = 'last_cleaning_time.txt';
my $time = time;
my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($file);
# Clean stats every two hours max
if (($time - $mtime) > 7200) {
# Create a job to expire dolos that need it
$job->app->minion->enqueue('expire_dolos');
# Months stats
my $dt = DateTime->from_epoch(epoch => $time);
$dt->subtract_duration(DateTime::Duration->new(months => $job->app->config('keep_hits')->{month_precision}));
$c->pg->db->query('DELETE FROM dolos_month WHERE year < ? OR (year = ? AND month < ?)', ($dt->year(), $dt->year(), $dt->month()));
# Weeks stats
$dt = DateTime->from_epoch(epoch => $time);
$dt->subtract_duration(DateTime::Duration->new(weeks => $job->app->config('keep_hits')->{week_precision}));
$c->pg->db->query('DELETE FROM dolos_week WHERE year < ? OR (year = ? AND week < ?)', ($dt->year(), $dt->year(), $dt->week_number()));
# Days stats
$dt = DateTime->from_epoch(epoch => $time);
$dt->subtract_duration(DateTime::Duration->new(days => $job->app->config('keep_hits')->{day_precision}));
$c->pg->db->query('DELETE FROM dolos_day WHERE year < ? OR (year = ? AND month < ?) OR (year = ? AND month = ? AND day < ?)', ($dt->year(), $dt->year(), $dt->month(), $dt->year(), $dt->month(), $dt->day_of_month()));
# Uber precision stats
$c->pg->db->query("DELETE FROM dolos_hits WHERE ts < (CURRENT_TIMESTAMP - INTERVAL '".$job->app->config('keep_hits')->{uber_precision}." days')");
Mojo::File->new($file)->spurt($job->app->config('keep_hits')->{uber_precision});
}
# Create a job to expire dolos that need it
$job->app->minion->enqueue('expire_dolos');
# Months stats
my $dt = DateTime->from_epoch(epoch => $time);
$dt->subtract_duration(DateTime::Duration->new(months => $job->app->config('keep_hits')->{month_precision}));
$c->pg->db->query('DELETE FROM dolos_month WHERE year < ? OR (year = ? AND month < ?)', ($dt->year(), $dt->year(), $dt->month()));
# Weeks stats
$dt = DateTime->from_epoch(epoch => $time);
$dt->subtract_duration(DateTime::Duration->new(weeks => $job->app->config('keep_hits')->{week_precision}));
$c->pg->db->query('DELETE FROM dolos_week WHERE year < ? OR (year = ? AND week < ?)', ($dt->year(), $dt->year(), $dt->week_number()));
# Days stats
$dt = DateTime->from_epoch(epoch => $time);
$dt->subtract_duration(DateTime::Duration->new(days => $job->app->config('keep_hits')->{day_precision}));
$c->pg->db->query('DELETE FROM dolos_day WHERE year < ? OR (year = ? AND month < ?) OR (year = ? AND month = ? AND day < ?)', ($dt->year(), $dt->year(), $dt->month(), $dt->year(), $dt->month(), $dt->day_of_month()));
# Uber precision stats
$c->pg->db->query("DELETE FROM dolos_hits WHERE ts < (CURRENT_TIMESTAMP - INTERVAL '".$job->app->config('keep_hits')->{uber_precision}." days')");
}
);
$self->app->minion->add_task(
......@@ -418,9 +414,6 @@ sub startup {
$migrations->from_file('utilities/migrations.sql')->migrate(1);
}
## Be sure last_cleaning_time.txt file exists
Mojo::File->new('last_cleaning_time.txt')->spurt(time) unless -e 'last_cleaning_time.txt';
## Router
my $r = $self->routes;
......
......@@ -13,6 +13,7 @@ sub register {
$app->helper(active => \&_active);
$app->helper(shortener => \&_shortener);
$app->helper(available_langs => \&_available_langs);
$app->helper(time_to_clean => \&_time_to_clean);
$app->hook(
before_dispatch => sub {
......@@ -64,4 +65,19 @@ sub _available_langs {
)->to_array;
}
sub _time_to_clean {
my $c = shift;
my $time = time;
state $last_cleaning;
# If the last cleaning was less than 2 hours ago
if (defined $last_cleaning && ($last_cleaning + 7200 > $time)) {
return 0
}
$last_cleaning = $time;
return 1;
}
1;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
/*!
* Bootstrap v3.3.7 (http://getbootstrap.com)
* Copyright 2011-2017 Twitter, Inc.
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
......
/*!
* Bootstrap v3.3.7 (http://getbootstrap.com)
* Copyright 2011-2017 Twitter, Inc.
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
......
/*!
* Bootstrap v3.3.7 (http://getbootstrap.com)
* Copyright 2011-2017 Twitter, Inc.
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
......
/*!
* Bootstrap v3.3.7 (http://getbootstrap.com)
* Copyright 2011-2017 Twitter, Inc.
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
......
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?25019046');
src: url('../font/fontello.eot?25019046#iefix') format('embedded-opentype'),
url('../font/fontello.svg?25019046#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'fontello';
src: url('data:application/octet-stream;base64,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') format('woff'),
......@@ -52,4 +44,4 @@
/* Uncomment for 3D effect */
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
.icon-language:before { content: '\e801'; } /* '' */
\ No newline at end of file
.icon-language:before { content: '\e801'; } /* '' */
/*!
* Bootstrap v3.3.7 (http://getbootstrap.com)
* Copyright 2011-2017 Twitter, Inc.
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
......@@ -620,6 +620,527 @@ if (typeof jQuery === 'undefined') {
}(jQuery);
/* ========================================================================
* Bootstrap: tooltip.js v3.3.7
* http://getbootstrap.com/javascript/#tooltip
* Inspired by the original jQuery.tipsy by Jason Frame
* ========================================================================
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// TOOLTIP PUBLIC CLASS DEFINITION
// ===============================
var Tooltip = function (element, options) {
this.type = null
this.options = null
this.enabled = null
this.timeout = null
this.hoverState = null
this.$element = null
this.inState = null
this.init('tooltip', element, options)
}
Tooltip.VERSION = '3.3.7'
Tooltip.TRANSITION_DURATION = 150
Tooltip.DEFAULTS = {
animation: true,
placement: 'top',
selector: false,
template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
trigger: 'hover focus',
title: '',
delay: 0,
html: false,
container: false,
viewport: {
selector: 'body',
padding: 0
}
}
Tooltip.prototype.init = function (type, element, options) {
this.enabled = true
this.type = type
this.$element = $(element)
this.options = this.getOptions(options)
this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))
this.inState = { click: false, hover: false, focus: false }
if (this.$element[0] instanceof document.constructor && !this.options.selector) {
throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!')
}
var triggers = this.options.trigger.split(' ')
for (var i = triggers.length; i--;) {
var trigger = triggers[i]
if (trigger == 'click') {
this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
} else if (trigger != 'manual') {
var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin'
var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
}
}
this.options.selector ?
(this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
this.fixTitle()
}
Tooltip.prototype.getDefaults = function () {
return Tooltip.DEFAULTS
}
Tooltip.prototype.getOptions = function (options) {
options = $.extend({}, this.getDefaults(), this.$element.data(), options)
if (options.delay && typeof options.delay == 'number') {
options.delay = {
show: options.delay,
hide: options.delay
}
}
return options
}
Tooltip.prototype.getDelegateOptions = function () {
var options = {}
var defaults = this.getDefaults()
this._options && $.each(this._options, function (key, value) {
if (defaults[key] != value) options[key] = value
})
return options
}
Tooltip.prototype.enter = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget).data('bs.' + this.type)
if (!self) {
self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
$(obj.currentTarget).data('bs.' + this.type, self)
}
if (obj instanceof $.Event) {
self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true
}
if (self.tip().hasClass('in') || self.hoverState == 'in') {
self.hoverState = 'in'
return
}
clearTimeout(self.timeout)
self.hoverState = 'in'
if (!self.options.delay || !self.options.delay.show) return self.show()
self.timeout = setTimeout(function () {
if (self.hoverState == 'in') self.show()
}, self.options.delay.show)
}
Tooltip.prototype.isInStateTrue = function () {
for (var key in this.inState) {
if (this.inState[key]) return true
}
return false
}
Tooltip.prototype.leave = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget).data('bs.' + this.type)
if (!self) {
self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
$(obj.currentTarget).data('bs.' + this.type, self)
}
if (obj instanceof $.Event) {
self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false
}
if (self.isInStateTrue()) return
clearTimeout(self.timeout)
self.hoverState = 'out'
if (!self.options.delay || !self.options.delay.hide) return self.hide()
self.timeout = setTimeout(function () {
if (self.hoverState == 'out') self.hide()
}, self.options.delay.hide)
}
Tooltip.prototype.show = function () {
var e = $.Event('show.bs.' + this.type)
if (this.hasContent() && this.enabled) {
this.$element.trigger(e)
var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])
if (e.isDefaultPrevented() || !inDom) return
var that = this
var $tip = this.tip()
var tipId = this.getUID(this.type)
this.setContent()
$tip.attr('id', tipId)
this.$element.attr('aria-describedby', tipId)
if (this.options.animation) $tip.addClass('fade')
var placement = typeof this.options.placement == 'function' ?
this.options.placement.call(this, $tip[0], this.$element[0]) :
this.options.placement
var autoToken = /\s?auto?\s?/i
var autoPlace = autoToken.test(placement)
if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
$tip
.detach()
.css({ top: 0, left: 0, display: 'block' })
.addClass(placement)
.data('bs.' + this.type, this)
this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
this.$element.trigger('inserted.bs.' + this.type)
var pos = this.getPosition()
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
if (autoPlace) {
var orgPlacement = placement
var viewportDim = this.getPosition(this.$viewport)
placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' :
placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' :
placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' :
placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' :
placement
$tip
.removeClass(orgPlacement)
.addClass(placement)
}
var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
this.applyPlacement(calculatedOffset, placement)
var complete = function () {
var prevHoverState = that.hoverState
that.$element.trigger('shown.bs.' + that.type)
that.hoverState = null
if (prevHoverState == 'out') that.leave(that)
}
$.support.transition && this.$tip.hasClass('fade') ?
$tip
.one('bsTransitionEnd', complete)
.emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
complete()
}
}
Tooltip.prototype.applyPlacement = function (offset, placement) {
var $tip = this.tip()
var width = $tip[0].offsetWidth
var height = $tip[0].offsetHeight
// manually read margins because getBoundingClientRect includes difference
var marginTop = parseInt($tip.css('margin-top'), 10)
var marginLeft = parseInt($tip.css('margin-left'), 10)
// we must check for NaN for ie 8/9
if (isNaN(marginTop)) marginTop = 0
if (isNaN(marginLeft)) marginLeft = 0
offset.top += marginTop
offset.left += marginLeft
// $.fn.offset doesn't round pixel values
// so we use setOffset directly with our own function B-0
$.offset.setOffset($tip[0], $.extend({
using: function (props) {
$tip.css({
top: Math.round(props.top),
left: Math.round(props.left)
})
}
}, offset), 0)
$tip.addClass('in')
// check to see if placing tip in new offset caused the tip to resize itself
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
if (placement == 'top' && actualHeight != height) {
offset.top = offset.top + height - actualHeight
}
var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)
if (delta.left) offset.left += delta.left
else offset.top += delta.top
var isVertical = /top|bottom/.test(placement)
var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'
$tip.offset(offset)
this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical)
}
Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) {
this.arrow()
.css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
.css(isVertical ? 'top' : 'left', '')
}
Tooltip.prototype.setContent = function () {
var $tip = this.tip()
var title = this.getTitle()
$tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
$tip.removeClass('fade in top bottom left right')
}
Tooltip.prototype.hide = function (callback) {
var that = this
var $tip = $(this.$tip)
var e = $.Event('hide.bs.' + this.type)
function complete() {
if (that.hoverState != 'in') $tip.detach()
if (that.$element) { // TODO: Check whether guarding this code with this `if` is really necessary.
that.$element
.removeAttr('aria-describedby')
.trigger('hidden.bs.' + that.type)
}