puppet-herald 0.2.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/.gitignore +3 -0
- data/.jshintrc +19 -0
- data/Gemfile +25 -12
- data/Gemfile.local.example +3 -0
- data/Guardfile +57 -0
- data/README.md +1 -1
- data/Rakefile +100 -20
- data/db/migrate/20141218200108_remove_no_of_reports_from_nodes.rb +7 -0
- data/lib/puppet-herald.rb +95 -65
- data/lib/puppet-herald/app/api.rb +79 -12
- data/lib/puppet-herald/app/configuration.rb +47 -15
- data/lib/puppet-herald/app/frontend.rb +6 -6
- data/lib/puppet-herald/app/views/app.erb +14 -20
- data/lib/puppet-herald/app/views/err500.erb +8 -5
- data/lib/puppet-herald/application.rb +9 -1
- data/lib/puppet-herald/cli.rb +5 -11
- data/lib/puppet-herald/database.rb +1 -7
- data/lib/puppet-herald/javascript.rb +16 -10
- data/lib/puppet-herald/models.rb +47 -0
- data/lib/puppet-herald/models/log-entry.rb +2 -0
- data/lib/puppet-herald/models/node.rb +49 -2
- data/lib/puppet-herald/models/report.rb +33 -9
- data/lib/puppet-herald/project.js +46 -0
- data/lib/puppet-herald/public/app.js +11 -9
- data/lib/puppet-herald/public/bower.json +10 -3
- data/lib/puppet-herald/public/components/artifact/artifact-directive.js +4 -0
- data/lib/puppet-herald/public/components/artifact/artifact.js +1 -3
- data/lib/puppet-herald/public/components/directives/directives.js +5 -1
- data/lib/puppet-herald/public/components/directives/status-button.html +1 -1
- data/lib/puppet-herald/public/components/directives/status-button.js +11 -11
- data/lib/puppet-herald/public/components/filters/filters.js +6 -2
- data/lib/puppet-herald/public/components/page.js +2 -2
- data/lib/puppet-herald/public/components/pagination.js +142 -0
- data/lib/puppet-herald/public/components/settings.js +25 -0
- data/lib/puppet-herald/public/css/herald.css +100 -3
- data/lib/puppet-herald/public/general/app.html +73 -0
- data/lib/puppet-herald/public/img/shield97-white.svg +53 -0
- data/lib/puppet-herald/public/img/shield97.png +0 -0
- data/lib/puppet-herald/public/node/node.html +27 -9
- data/lib/puppet-herald/public/node/node.js +43 -15
- data/lib/puppet-herald/public/nodes/nodes.html +25 -7
- data/lib/puppet-herald/public/nodes/nodes.js +29 -14
- data/lib/puppet-herald/public/report/report.html +60 -13
- data/lib/puppet-herald/public/report/report.js +21 -14
- data/lib/puppet-herald/public/router.js +55 -0
- data/lib/puppet-herald/purgecronjob.rb +35 -0
- data/lib/puppet-herald/version.rb +2 -2
- data/package.json +14 -16
- data/puppet-herald.gemspec +12 -7
- data/spec/integration/app/configuration_spec.rb +33 -0
- data/spec/integration/application_spec.rb +139 -20
- data/spec/integration/fixtures/nodes.yml +13 -0
- data/spec/integration/fixtures/pending-notify.yaml +346 -0
- data/spec/integration/fixtures/reports.yml +61 -0
- data/spec/integration/models/node_spec.rb +12 -3
- data/spec/integration/models/report_spec.rb +60 -4
- data/spec/spec_helper.rb +6 -10
- data/spec/support/active_record.rb +1 -0
- data/spec/support/fixtures.rb +16 -0
- data/spec/unit/puppet-herald/cli_spec.rb +4 -4
- data/spec/unit/puppet-herald/database_spec.rb +5 -3
- data/spec/unit/puppet-herald/purgecronjob_spec.rb +37 -0
- data/test/javascript/.bowerrc +3 -0
- data/test/javascript/bower.json +21 -0
- data/test/javascript/karma.conf.js +17 -22
- data/test/javascript/src/app_test.js +10 -61
- data/test/javascript/src/components/directives/status-button_test.js +10 -10
- data/test/javascript/src/components/paginate_test.js +183 -0
- data/test/javascript/src/node/node_test.js +16 -6
- data/test/javascript/src/nodes/nodes_test.js +14 -2
- data/test/javascript/src/report/report_test.js +6 -6
- data/test/javascript/src/router_test.js +79 -0
- metadata +642 -23
Binary file
|
@@ -1,7 +1,13 @@
|
|
1
|
-
<div
|
1
|
+
<div ui-view>
|
2
|
+
|
3
|
+
<div class="panel panel-default">
|
2
4
|
<!-- Default panel contents -->
|
3
|
-
<div class="panel-heading">
|
4
|
-
<samp>{{ ctrl.node.name }}</samp> <span class="badge"
|
5
|
+
<div class="panel-heading">All reports for:
|
6
|
+
<samp>{{ ctrl.node.name }}</samp> <span class="badge" ng-pluralize
|
7
|
+
count="ctrl.pagination.elements()"
|
8
|
+
when="{'0': 'no reports',
|
9
|
+
'1': '1 report',
|
10
|
+
'other': '{} reports'}"></span>
|
5
11
|
</div>
|
6
12
|
|
7
13
|
<!-- Table -->
|
@@ -16,17 +22,29 @@
|
|
16
22
|
</tr>
|
17
23
|
</thead>
|
18
24
|
<tbody>
|
19
|
-
<tr
|
25
|
+
<tr
|
26
|
+
dir-paginate="report in ctrl.node.reports | itemsPerPage: ctrl.pagination.limit() | orderBy: '-time'"
|
27
|
+
total-items="ctrl.pagination.elements()">
|
20
28
|
<td>{{ $index + 1 }}</td>
|
21
|
-
<td>{{ report.configuration_version }}</td>
|
22
|
-
<td><
|
29
|
+
<td><samp>{{ report.configuration_version }}</samp></td>
|
30
|
+
<td><ws-status-button
|
23
31
|
status="report.status"
|
24
32
|
id="report.id"
|
25
|
-
|
26
|
-
|
33
|
+
idname="'reportId'"
|
34
|
+
route="'report'"
|
35
|
+
></ws-status-button></td>
|
27
36
|
<td>{{ report.environment }}</td>
|
28
|
-
<td
|
37
|
+
<td title="{{ report.time }}">{{ report.time | amCalendar }}</td>
|
29
38
|
</tr>
|
30
39
|
</tbody>
|
31
40
|
</table>
|
41
|
+
</div>
|
42
|
+
|
43
|
+
<nav class="text-center">
|
44
|
+
<dir-pagination-controls
|
45
|
+
on-page-change="ctrl.onPageChange(newPageNumber)"
|
46
|
+
boundary-links="true"
|
47
|
+
></dir-pagination-controls>
|
48
|
+
</nav>
|
49
|
+
|
32
50
|
</div>
|
@@ -1,27 +1,55 @@
|
|
1
|
+
(function() {
|
2
|
+
|
1
3
|
'use strict';
|
2
4
|
|
3
5
|
angular.module('herald.node', [
|
4
|
-
'
|
6
|
+
'ui.router',
|
5
7
|
'herald.page',
|
6
8
|
'herald.directives',
|
9
|
+
'herald.pagination',
|
7
10
|
'angularMoment'
|
8
11
|
])
|
9
12
|
|
10
|
-
.
|
11
|
-
|
12
|
-
|
13
|
-
controller: 'NodeController'
|
14
|
-
});
|
15
|
-
}])
|
13
|
+
.controller('NodeController',
|
14
|
+
['$http', '$stateParams', 'Page', 'PaginationFactory',
|
15
|
+
function($http, $stateParams, Page, PaginationFactory) {
|
16
16
|
|
17
|
-
.controller('NodeController', ['$http', '$routeParams', 'Page', function($http, $routeParams, Page) {
|
18
17
|
var ctrl = this;
|
18
|
+
Page.title('Reports');
|
19
|
+
ctrl.pagination = PaginationFactory.DEFAULT;
|
20
|
+
ctrl.cache = PaginationFactory.createPageCache(60); // 60 seconds cache
|
19
21
|
ctrl.node = null;
|
20
|
-
|
21
|
-
|
22
|
+
ctrl.nav = {
|
23
|
+
node: null,
|
24
|
+
report: null
|
25
|
+
};
|
26
|
+
this.nodeId = $stateParams.nodeId;
|
27
|
+
var gateway = '/api/v1/nodes/' + this.nodeId;
|
28
|
+
|
29
|
+
function setNode(nodeData) {
|
30
|
+
ctrl.node = nodeData;
|
31
|
+
ctrl.nav.node = ctrl.node.name;
|
32
|
+
}
|
22
33
|
|
23
|
-
|
24
|
-
ctrl.
|
25
|
-
|
26
|
-
|
27
|
-
|
34
|
+
function getResultsPage(pageNumber) {
|
35
|
+
ctrl.pagination.page(pageNumber);
|
36
|
+
if (ctrl.cache.isLoaded(pageNumber)) {
|
37
|
+
setNode(ctrl.cache.get(pageNumber));
|
38
|
+
return;
|
39
|
+
}
|
40
|
+
var config = { headers: ctrl.pagination.toHeaders() };
|
41
|
+
$http.get(gateway, config).success(function(data, status, headers, config) {
|
42
|
+
setNode(data);
|
43
|
+
ctrl.pagination = PaginationFactory.fromHeaders(headers);
|
44
|
+
var loadedPage = ctrl.pagination.page();
|
45
|
+
Page.title('Reports for', data.name);
|
46
|
+
ctrl.cache.set(loadedPage, data);
|
47
|
+
});
|
48
|
+
}
|
49
|
+
getResultsPage(ctrl.pagination.page());
|
50
|
+
ctrl.onPageChange = function(newPage) {
|
51
|
+
getResultsPage(newPage);
|
52
|
+
};
|
53
|
+
}]);
|
54
|
+
|
55
|
+
})();
|
@@ -1,6 +1,11 @@
|
|
1
|
-
<div
|
1
|
+
<div ui-view>
|
2
|
+
<div class="panel panel-default">
|
2
3
|
<!-- Default panel contents -->
|
3
|
-
<div class="panel-heading">
|
4
|
+
<div class="panel-heading">All Puppet nodes <span class="badge" ng-pluralize
|
5
|
+
count="ctrl.pagination.elements()"
|
6
|
+
when="{'0': 'none',
|
7
|
+
'1': '1 node',
|
8
|
+
'other': '{} nodes'}"></span></div>
|
4
9
|
|
5
10
|
<!-- Table -->
|
6
11
|
<table class="table table-striped table-hover">
|
@@ -14,16 +19,29 @@
|
|
14
19
|
</tr>
|
15
20
|
</thead>
|
16
21
|
<tbody>
|
17
|
-
<tr
|
22
|
+
<tr
|
23
|
+
dir-paginate="node in ctrl.all | itemsPerPage: ctrl.pagination.limit() | orderBy:'-last_run'"
|
24
|
+
total-items="ctrl.pagination.elements()"
|
25
|
+
>
|
18
26
|
<td>{{ $index + 1 }}</td>
|
19
|
-
<td>{{ node.name }}</td>
|
20
|
-
<td><
|
27
|
+
<td><samp>{{ node.name }}</samp></td>
|
28
|
+
<td><ws-status-button
|
21
29
|
status="node.status"
|
22
30
|
id="node.id"
|
23
|
-
|
31
|
+
idname="'nodeId'"
|
32
|
+
route="'node'"></ws-status-button></td>
|
24
33
|
<td>{{ node.no_of_reports }}</td>
|
25
|
-
<td
|
34
|
+
<td title="{{ node.last_run }}">{{ node.last_run | amCalendar }}</td>
|
26
35
|
</tr>
|
27
36
|
</tbody>
|
28
37
|
</table>
|
38
|
+
</div>
|
39
|
+
|
40
|
+
<nav class="text-center">
|
41
|
+
<dir-pagination-controls
|
42
|
+
on-page-change="ctrl.onPageChange(newPageNumber)"
|
43
|
+
boundary-links="true"
|
44
|
+
></dir-pagination-controls>
|
45
|
+
</nav>
|
46
|
+
|
29
47
|
</div>
|
@@ -1,25 +1,40 @@
|
|
1
|
+
(function() {
|
2
|
+
|
1
3
|
'use strict';
|
2
4
|
|
3
5
|
angular.module('herald.nodes', [
|
4
|
-
'ngRoute',
|
5
6
|
'herald.directives',
|
6
7
|
'herald.page',
|
8
|
+
'herald.pagination',
|
7
9
|
'angularMoment'
|
8
10
|
])
|
9
11
|
|
10
|
-
.
|
11
|
-
|
12
|
-
templateUrl: 'nodes/nodes.html',
|
13
|
-
controller: 'NodesController'
|
14
|
-
});
|
15
|
-
}])
|
16
|
-
|
17
|
-
.controller('NodesController', ['$http', 'Page', function($http, Page) {
|
18
|
-
Page.title('All nodes');
|
12
|
+
.controller('NodesController', ['$http', 'Page', 'PaginationFactory', function($http, Page, PaginationFactory) {
|
13
|
+
Page.title('All Puppet nodes');
|
19
14
|
var ctrl = this;
|
15
|
+
ctrl.title =
|
16
|
+
ctrl.pagination = PaginationFactory.DEFAULT;
|
17
|
+
ctrl.cache = PaginationFactory.createPageCache(60); // 60 seconds cache
|
20
18
|
ctrl.all = [];
|
21
19
|
|
22
|
-
|
23
|
-
ctrl.
|
24
|
-
|
25
|
-
|
20
|
+
function getResultsPage(pageNumber) {
|
21
|
+
ctrl.pagination.page(pageNumber);
|
22
|
+
if (ctrl.cache.isLoaded(pageNumber)) {
|
23
|
+
ctrl.all = ctrl.cache.get(pageNumber);
|
24
|
+
return;
|
25
|
+
}
|
26
|
+
var config = { headers: ctrl.pagination.toHeaders() };
|
27
|
+
$http.get('/api/v1/nodes', config).success(function(data, status, headers, config) {
|
28
|
+
ctrl.all = data;
|
29
|
+
ctrl.pagination = PaginationFactory.fromHeaders(headers);
|
30
|
+
var loadedPage = ctrl.pagination.page();
|
31
|
+
ctrl.cache.set(loadedPage, data);
|
32
|
+
});
|
33
|
+
}
|
34
|
+
getResultsPage(ctrl.pagination.page());
|
35
|
+
ctrl.onPageChange = function(newPage) {
|
36
|
+
getResultsPage(newPage);
|
37
|
+
};
|
38
|
+
}]);
|
39
|
+
|
40
|
+
})();
|
@@ -1,25 +1,72 @@
|
|
1
|
-
<div class="panel panel-
|
1
|
+
<div class="panel panel-default">
|
2
2
|
<!-- Default panel contents -->
|
3
|
-
<div class="panel-heading">
|
4
|
-
|
5
|
-
<span class="
|
3
|
+
<div class="panel-heading">
|
4
|
+
|
5
|
+
<span class="">
|
6
|
+
Report:
|
7
|
+
<samp>{{ ctrl.report.configuration_version }}</samp>
|
8
|
+
<span class="badge" ng-pluralize
|
9
|
+
count="ctrl.report.log_entries.length"
|
10
|
+
when="{'0': 'no output',
|
11
|
+
'1': '1 line',
|
12
|
+
'other': '{} lines'}"></span>
|
13
|
+
</span>
|
14
|
+
|
15
|
+
<div class="btn-group"> </div>
|
16
|
+
Show more information:
|
17
|
+
<div class="btn-group">
|
18
|
+
<button
|
19
|
+
type="button"
|
20
|
+
class="btn btn-default btn-sm dropdown-toggle"
|
21
|
+
ng-init="dropdown = false"
|
22
|
+
ng-click="dropdown = !dropdown">
|
23
|
+
{{ settings.report.info ? 'Yes' : 'No' }} <span class="caret"></span>
|
24
|
+
</button>
|
25
|
+
<ul
|
26
|
+
class="dropdown-menu"
|
27
|
+
role="menu"
|
28
|
+
ng-click="event.preverntDefault();"
|
29
|
+
ng-class="{'ws-show-block': dropdown}">
|
30
|
+
<li><a href="" ng-click="settings.report.info = true; dropdown = !dropdown">Yes</a></li>
|
31
|
+
<li><a href="" ng-click="settings.report.info = false; dropdown = !dropdown">No</a></li>
|
32
|
+
</ul>
|
33
|
+
</div>
|
6
34
|
</div>
|
7
35
|
|
8
|
-
|
9
|
-
|
10
|
-
|
36
|
+
<table class="table table-hover" ng-show="settings.report.info">
|
37
|
+
<tbody>
|
38
|
+
<tr>
|
39
|
+
<th>Host</th>
|
40
|
+
<td><samp>{{ ctrl.report.host }}</samp></td>
|
41
|
+
<th>Status</th>
|
42
|
+
<td><span class="label label-{{ ctrl.report.status | colorizeStatus }}">{{ ctrl.report.status | uppercase }}</span></td>
|
43
|
+
</tr>
|
11
44
|
<tr>
|
12
|
-
<th
|
13
|
-
<
|
45
|
+
<th>Date of report</th>
|
46
|
+
<td title="{{ ctrl.report.time }}">{{ ctrl.report.time | amCalendar }}</td>
|
47
|
+
<th>Configuration version</th>
|
48
|
+
<td><samp>{{ ctrl.report.configuration_version }}</samp></td>
|
14
49
|
</tr>
|
15
|
-
|
50
|
+
<tr>
|
51
|
+
<th>Environment</th>
|
52
|
+
<td>{{ ctrl.report.environment }}</td>
|
53
|
+
<th>Puppet version</th>
|
54
|
+
<td><samp>{{ ctrl.report.puppet_version }}</samp></td>
|
55
|
+
</tr>
|
56
|
+
</tbody>
|
57
|
+
</table>
|
58
|
+
|
59
|
+
<!-- Table -->
|
60
|
+
<table
|
61
|
+
class="table table-condensed table-hover herald-table-report"
|
62
|
+
ng-class="{puppet3: settings.colorSyntax.selected.value == 'puppet3'}">
|
16
63
|
<tbody>
|
17
64
|
<tr>
|
18
|
-
<td></td>
|
19
|
-
<td># puppet {{ ctrl.report.kind }} [...]</td>
|
65
|
+
<td class="unselectable"></td>
|
66
|
+
<td class="unselectable"># puppet {{ ctrl.report.kind }} [...]</td>
|
20
67
|
</tr>
|
21
68
|
<tr ng-repeat="entry in ctrl.report.log_entries | orderBy:'time'">
|
22
|
-
<td>{{ $index + 1 }}</td>
|
69
|
+
<td class="unselectable">{{ $index + 1 }}</td>
|
23
70
|
<td class="herald-log-{{ entry.level }}">{{ entry.level | capitalize }}: {{ entry.source }}: {{ entry.message }}</td>
|
24
71
|
</tr>
|
25
72
|
</tbody>
|
@@ -1,26 +1,33 @@
|
|
1
|
+
(function() {
|
2
|
+
|
1
3
|
'use strict';
|
2
4
|
|
3
5
|
angular.module('herald.report', [
|
4
|
-
'
|
6
|
+
'ui.router',
|
5
7
|
'herald.page',
|
6
8
|
'herald.directives'
|
7
9
|
])
|
8
10
|
|
9
|
-
.
|
10
|
-
$routeProvider.when('/report/:reportId', {
|
11
|
-
templateUrl: 'report/report.html',
|
12
|
-
controller: 'ReportController'
|
13
|
-
});
|
14
|
-
}])
|
15
|
-
|
16
|
-
.controller('ReportController', ['$http', '$routeParams', 'Page', function($http, $routeParams, Page) {
|
17
|
-
Page.title('Report');
|
11
|
+
.controller('ReportController', ['$http', '$stateParams', 'Page', function($http, $stateParams, Page) {
|
18
12
|
var ctrl = this;
|
13
|
+
Page.title('Puppet report');
|
19
14
|
ctrl.report = null;
|
20
|
-
|
15
|
+
ctrl.nav = {
|
16
|
+
node: null,
|
17
|
+
report: null
|
18
|
+
};
|
19
|
+
this.reportId = $stateParams.reportId;
|
20
|
+
|
21
|
+
function setReport(reportData) {
|
22
|
+
ctrl.report = reportData;
|
23
|
+
ctrl.nav.node = ctrl.report.host;
|
24
|
+
ctrl.nav.report = ctrl.report.configuration_version;
|
25
|
+
}
|
21
26
|
|
22
27
|
$http.get('/api/v1/reports/' + this.reportId).success(function(data) {
|
23
|
-
|
24
|
-
Page.title('
|
28
|
+
setReport(data);
|
29
|
+
Page.title('Puppet report', data.configuration_version);
|
25
30
|
});
|
26
|
-
}]);
|
31
|
+
}]);
|
32
|
+
|
33
|
+
})();
|
@@ -0,0 +1,55 @@
|
|
1
|
+
(function(){
|
2
|
+
'use strict';
|
3
|
+
|
4
|
+
var router = angular.module('herald.router', [
|
5
|
+
'ui.router',
|
6
|
+
'herald.nodes',
|
7
|
+
'herald.node',
|
8
|
+
'herald.report',
|
9
|
+
'ncy-angular-breadcrumb'
|
10
|
+
]);
|
11
|
+
|
12
|
+
router.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
|
13
|
+
$urlRouterProvider.otherwise('/');
|
14
|
+
$stateProvider
|
15
|
+
|
16
|
+
.state('home', {
|
17
|
+
url: '/',
|
18
|
+
abstract: true,
|
19
|
+
templateUrl: 'general/app.html'
|
20
|
+
})
|
21
|
+
|
22
|
+
.state('nodes', {
|
23
|
+
url: '',
|
24
|
+
templateUrl: 'nodes/nodes.html',
|
25
|
+
controller: 'NodesController as ctrl',
|
26
|
+
parent: 'home',
|
27
|
+
ncyBreadcrumb: {
|
28
|
+
label: ' '
|
29
|
+
}
|
30
|
+
})
|
31
|
+
|
32
|
+
.state('node', {
|
33
|
+
url: 'node-{nodeId:int}',
|
34
|
+
templateUrl: 'node/node.html',
|
35
|
+
controller: 'NodeController as ctrl',
|
36
|
+
parent: 'nodes',
|
37
|
+
ncyBreadcrumb: {
|
38
|
+
label: "All reports for: {{ ctrl.nav.node }}"
|
39
|
+
}
|
40
|
+
})
|
41
|
+
|
42
|
+
.state('report', {
|
43
|
+
parent: 'node',
|
44
|
+
url: '/report-{reportId:int}',
|
45
|
+
templateUrl: 'report/report.html',
|
46
|
+
controller: 'ReportController as ctrl',
|
47
|
+
ncyBreadcrumb: {
|
48
|
+
label: 'Report: {{ ctrl.nav.report }}'
|
49
|
+
}
|
50
|
+
})
|
51
|
+
|
52
|
+
;
|
53
|
+
}]);
|
54
|
+
|
55
|
+
})();
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# A module for Herald
|
2
|
+
module PuppetHerald
|
3
|
+
# A cron job for Herald
|
4
|
+
class PurgeCronJob
|
5
|
+
# Number of seconds in a day
|
6
|
+
#
|
7
|
+
# @return [Integer]
|
8
|
+
SECONDS_IN_DAY = 86_400
|
9
|
+
|
10
|
+
# Run a purge job
|
11
|
+
#
|
12
|
+
# @return [nil]
|
13
|
+
def run!
|
14
|
+
require 'puppet-herald'
|
15
|
+
require 'puppet-herald/models/report'
|
16
|
+
limit = ENV['PUPPET_HERALD_PURGE_LIMIT'] || '90d'
|
17
|
+
date = parse_limit limit
|
18
|
+
PuppetHerald.logger.info "Running a purge reports job with limit: `#{limit}` that is `#{date}`..."
|
19
|
+
reports = PuppetHerald::Models::Report.purge_older_then(date)
|
20
|
+
PuppetHerald.logger.info "Job completed. Purged: #{reports} reports."
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# Parse a limit and returns number of seconds
|
25
|
+
#
|
26
|
+
# @param limit [String] a limit as string
|
27
|
+
# @return [DateTime] a date in the past - now minus limit
|
28
|
+
def parse_limit(limit)
|
29
|
+
require 'rufus/scheduler'
|
30
|
+
seconds = Rufus::Scheduler.parse limit
|
31
|
+
now = DateTime.now
|
32
|
+
now - Rational(seconds, SECONDS_IN_DAY)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|