deleted unused folders
This commit is contained in:
parent
a597dfdb0a
commit
5243725e39
|
@ -1,34 +0,0 @@
|
||||||
/* Activity stream
|
|
||||||
* Handle the pagination for activity list
|
|
||||||
*
|
|
||||||
* Options
|
|
||||||
* - page: current page number
|
|
||||||
*/
|
|
||||||
this.ckan.module('activity-stream', function($) {
|
|
||||||
return {
|
|
||||||
|
|
||||||
/* Initialises the module setting up elements and event listeners.
|
|
||||||
*
|
|
||||||
* Returns nothing.
|
|
||||||
*/
|
|
||||||
initialize: function () {
|
|
||||||
$('#activity_types_filter_select').on(
|
|
||||||
'change',
|
|
||||||
this._onChangeActivityType
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/* Filter using the selected
|
|
||||||
* activity type
|
|
||||||
*
|
|
||||||
* Returns nothing
|
|
||||||
*/
|
|
||||||
_onChangeActivityType: function (event) {
|
|
||||||
// event.preventDefault();
|
|
||||||
url = $("#activity_types_filter_select option:selected" ).data('url');
|
|
||||||
window.location = url;
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
});
|
|
|
@ -1,103 +0,0 @@
|
||||||
.activity {
|
|
||||||
padding: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
background: transparent url("/dotted.png") 21px 0 repeat-y; }
|
|
||||||
.activity .item {
|
|
||||||
position: relative;
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
padding: 0; }
|
|
||||||
.activity .item .user-image {
|
|
||||||
border-radius: 100px; }
|
|
||||||
.activity .item .date {
|
|
||||||
color: #999;
|
|
||||||
font-size: 12px;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin: 5px 0 0 80px; }
|
|
||||||
.activity .item.no-avatar p {
|
|
||||||
margin-left: 40px; }
|
|
||||||
|
|
||||||
.activity_buttons > a.btn.disabled {
|
|
||||||
color: inherit;
|
|
||||||
opacity: 0.70 !important; }
|
|
||||||
|
|
||||||
.popover {
|
|
||||||
width: 300px; }
|
|
||||||
.popover .popover-title {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 0; }
|
|
||||||
.popover p.about {
|
|
||||||
margin: 0 0 10px 0; }
|
|
||||||
.popover .popover-close {
|
|
||||||
float: right;
|
|
||||||
text-decoration: none; }
|
|
||||||
.popover .empty {
|
|
||||||
padding: 10px;
|
|
||||||
color: #6e6e6e;
|
|
||||||
font-style: italic; }
|
|
||||||
|
|
||||||
.activity .item .icon {
|
|
||||||
color: #999999; }
|
|
||||||
.activity .item.failure .icon {
|
|
||||||
color: #B95252; }
|
|
||||||
.activity .item.success .icon {
|
|
||||||
color: #69A67A; }
|
|
||||||
.activity .item.added-tag .icon {
|
|
||||||
color: #6995a6; }
|
|
||||||
.activity .item.changed-group .icon {
|
|
||||||
color: #767DCE; }
|
|
||||||
.activity .item.changed-package .icon {
|
|
||||||
color: #8c76ce; }
|
|
||||||
.activity .item.changed-package_extra .icon {
|
|
||||||
color: #769ace; }
|
|
||||||
.activity .item.changed-resource .icon {
|
|
||||||
color: #aa76ce; }
|
|
||||||
.activity .item.changed-user .icon {
|
|
||||||
color: #76b8ce; }
|
|
||||||
.activity .item.changed-organization .icon {
|
|
||||||
color: #699fa6; }
|
|
||||||
.activity .item.deleted-group .icon {
|
|
||||||
color: #B95252; }
|
|
||||||
.activity .item.deleted-package .icon {
|
|
||||||
color: #b97452; }
|
|
||||||
.activity .item.deleted-package_extra .icon {
|
|
||||||
color: #b95274; }
|
|
||||||
.activity .item.deleted-resource .icon {
|
|
||||||
color: #b99752; }
|
|
||||||
.activity .item.deleted-organization .icon {
|
|
||||||
color: #b95297; }
|
|
||||||
.activity .item.new-group .icon {
|
|
||||||
color: #69A67A; }
|
|
||||||
.activity .item.new-package .icon {
|
|
||||||
color: #69a68e; }
|
|
||||||
.activity .item.new-package_extra .icon {
|
|
||||||
color: #6ca669; }
|
|
||||||
.activity .item.new-resource .icon {
|
|
||||||
color: #81a669; }
|
|
||||||
.activity .item.new-user .icon {
|
|
||||||
color: #69a6a3; }
|
|
||||||
.activity .item.new-organization .icon {
|
|
||||||
color: #81a669; }
|
|
||||||
.activity .item.removed-tag .icon {
|
|
||||||
color: #b95297; }
|
|
||||||
.activity .item.deleted-related-item .icon {
|
|
||||||
color: #b9b952; }
|
|
||||||
.activity .item.follow-dataset .icon {
|
|
||||||
color: #767DCE; }
|
|
||||||
.activity .item.follow-user .icon {
|
|
||||||
color: #8c76ce; }
|
|
||||||
.activity .item.new-related-item .icon {
|
|
||||||
color: #95a669; }
|
|
||||||
.activity .item.follow-group .icon {
|
|
||||||
color: #8ba669; }
|
|
||||||
|
|
||||||
.select-time {
|
|
||||||
width: 250px;
|
|
||||||
display: inline; }
|
|
||||||
|
|
||||||
br.line-height2 {
|
|
||||||
line-height: 2; }
|
|
||||||
|
|
||||||
.pull-right {
|
|
||||||
float: right; }
|
|
||||||
|
|
||||||
/*# sourceMappingURL=activity.css.map */
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"version": 3,
|
|
||||||
"mappings": "AAAA,SAAU;EACN,OAAO,EAAE,CAAC;EACV,eAAe,EAAE,IAAI;EACrB,UAAU,EAAE,8CAA+C;EAC3D,eAAM;IACF,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,UAAU;IAClB,OAAO,EAAE,CAAC;IACV,2BAAY;MACR,aAAa,EAAE,KAAK;IAExB,qBAAM;MACF,KAAK,EAAE,IAAI;MACX,SAAS,EAAE,IAAI;MACf,WAAW,EAAE,MAAM;MACnB,MAAM,EAAE,YAAY;IAExB,2BAAc;MAEV,WAAW,EAAE,IAAI;;AAK7B,kCAAmC;EAC/B,KAAK,EAAE,OAAO;EACd,OAAO,EAAE,eAAe;;AAG5B,QAAS;EACL,KAAK,EAAE,KAAK;EACZ,uBAAe;IACX,WAAW,EAAE,IAAI;IACjB,aAAa,EAAE,CAAC;EAEpB,gBAAQ;IACJ,MAAM,EAAE,UAAU;EAEtB,uBAAe;IACX,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,IAAI;EAEzB,eAAO;IACH,OAAO,EAAE,IAAI;IACb,KAAK,EAAE,OAAO;IACd,UAAU,EAAE,MAAM;;AAatB,qBAAM;EACF,KAAK,EARQ,OAAO;AAUxB,6BAAgB;EACZ,KAAK,EAPS,OAAO;AASzB,6BAAgB;EACZ,KAAK,EAbM,OAAO;AAetB,+BAAkB;EACd,KAAK,EAAE,OAAiC;AAE5C,mCAAsB;EAClB,KAAK,EAjBS,OAAO;AAmBzB,qCAAwB;EACpB,KAAK,EAAE,OAAoC;AAE/C,2CAA8B;EAC1B,KAAK,EAAE,OAAqC;AAEhD,sCAAyB;EACrB,KAAK,EAAE,OAAoC;AAE/C,kCAAqB;EACjB,KAAK,EAAE,OAAqC;AAEhD,0CAA6B;EACzB,KAAK,EAAE,OAAiC;AAE5C,mCAAsB;EAClB,KAAK,EAlCS,OAAO;AAoCzB,qCAAwB;EACpB,KAAK,EAAE,OAAoC;AAE/C,2CAA8B;EAC1B,KAAK,EAAE,OAAqC;AAEhD,sCAAyB;EACrB,KAAK,EAAE,OAAoC;AAE/C,0CAA6B;EACzB,KAAK,EAAE,OAAqC;AAEhD,+BAAkB;EACd,KAAK,EApDM,OAAO;AAsDtB,iCAAoB;EAChB,KAAK,EAAE,OAAiC;AAE5C,uCAA0B;EACtB,KAAK,EAAE,OAAkC;AAE7C,kCAAqB;EACjB,KAAK,EAAE,OAAkC;AAE7C,8BAAiB;EACb,KAAK,EAAE,OAAiC;AAE5C,sCAAyB;EACrB,KAAK,EAAE,OAAkC;AAE7C,iCAAoB;EAChB,KAAK,EAAE,OAAqC;AAEhD,0CAA6B;EACzB,KAAK,EAAE,OAAoC;AAE/C,oCAAuB;EACnB,KAAK,EA3EU,OAAO;AA6E1B,iCAAoB;EAChB,KAAK,EAAE,OAAqC;AAEhD,sCAAyB;EACrB,KAAK,EAAE,OAAkC;AAE7C,kCAAqB;EACjB,KAAK,EAAE,OAAkC;;AAIjD,YAAa;EACX,KAAK,EAAE,KAAK;EACZ,OAAO,EAAE,MAAM;;AAGjB,eAAgB;EACd,WAAW,EAAE,CAAC;;AAGhB,WAAY;EACR,KAAK,EAAE,KAAK",
|
|
||||||
"sources": ["activity.scss"],
|
|
||||||
"names": [],
|
|
||||||
"file": "activity.css"
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
.activity {
|
|
||||||
padding: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
background: transparent url('/dotted.png') 21px 0 repeat-y;
|
|
||||||
.item {
|
|
||||||
position: relative;
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
padding: 0;
|
|
||||||
.user-image {
|
|
||||||
border-radius: 100px;
|
|
||||||
}
|
|
||||||
.date {
|
|
||||||
color: #999;
|
|
||||||
font-size: 12px;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin: 5px 0 0 80px;
|
|
||||||
}
|
|
||||||
&.no-avatar p {
|
|
||||||
// Use by datapusher
|
|
||||||
margin-left: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity_buttons > a.btn.disabled {
|
|
||||||
color: inherit;
|
|
||||||
opacity: 0.70 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For profile information that appears in the popover
|
|
||||||
.popover {
|
|
||||||
width: 300px;
|
|
||||||
.popover-title {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
p.about {
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
}
|
|
||||||
.popover-close {
|
|
||||||
float: right;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.empty {
|
|
||||||
padding: 10px;
|
|
||||||
color: #6e6e6e;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activity Stream base colors
|
|
||||||
$activityColorText: #FFFFFF;
|
|
||||||
$activityColorBlank: #999999;
|
|
||||||
$activityColorNew: #69A67A;
|
|
||||||
$activityColorNeutral: #767DCE;
|
|
||||||
$activityColorModify: #767DCE;
|
|
||||||
$activityColorDelete: #B95252;
|
|
||||||
|
|
||||||
.activity .item {
|
|
||||||
.icon {
|
|
||||||
color: $activityColorBlank ;
|
|
||||||
} // Non defined
|
|
||||||
&.failure .icon {
|
|
||||||
color: $activityColorDelete;
|
|
||||||
}
|
|
||||||
&.success .icon {
|
|
||||||
color: $activityColorNew;
|
|
||||||
}
|
|
||||||
&.added-tag .icon {
|
|
||||||
color: adjust-hue($activityColorNew, 60);
|
|
||||||
}
|
|
||||||
&.changed-group .icon {
|
|
||||||
color: $activityColorModify;
|
|
||||||
}
|
|
||||||
&.changed-package .icon {
|
|
||||||
color: adjust-hue($activityColorModify, 20);
|
|
||||||
}
|
|
||||||
&.changed-package_extra .icon {
|
|
||||||
color: adjust-hue($activityColorModify, -20);
|
|
||||||
}
|
|
||||||
&.changed-resource .icon {
|
|
||||||
color: adjust-hue($activityColorModify, 40);
|
|
||||||
}
|
|
||||||
&.changed-user .icon {
|
|
||||||
color: adjust-hue($activityColorModify, -40);
|
|
||||||
}
|
|
||||||
&.changed-organization .icon {
|
|
||||||
color: adjust-hue($activityColorNew, 50);
|
|
||||||
}
|
|
||||||
&.deleted-group .icon {
|
|
||||||
color: $activityColorDelete;
|
|
||||||
}
|
|
||||||
&.deleted-package .icon {
|
|
||||||
color: adjust-hue($activityColorDelete, 20);
|
|
||||||
}
|
|
||||||
&.deleted-package_extra .icon {
|
|
||||||
color: adjust-hue($activityColorDelete, -20);
|
|
||||||
}
|
|
||||||
&.deleted-resource .icon {
|
|
||||||
color: adjust-hue($activityColorDelete, 40);
|
|
||||||
}
|
|
||||||
&.deleted-organization .icon {
|
|
||||||
color: adjust-hue($activityColorDelete, -40);
|
|
||||||
}
|
|
||||||
&.new-group .icon {
|
|
||||||
color: $activityColorNew;
|
|
||||||
}
|
|
||||||
&.new-package .icon {
|
|
||||||
color: adjust-hue($activityColorNew, 20);
|
|
||||||
}
|
|
||||||
&.new-package_extra .icon {
|
|
||||||
color: adjust-hue($activityColorNew, -20);
|
|
||||||
}
|
|
||||||
&.new-resource .icon {
|
|
||||||
color: adjust-hue($activityColorNew, -40);
|
|
||||||
}
|
|
||||||
&.new-user .icon {
|
|
||||||
color: adjust-hue($activityColorNew, 40);
|
|
||||||
}
|
|
||||||
&.new-organization .icon {
|
|
||||||
color: adjust-hue($activityColorNew, -40);
|
|
||||||
}
|
|
||||||
&.removed-tag .icon {
|
|
||||||
color: adjust-hue($activityColorDelete, -40);
|
|
||||||
}
|
|
||||||
&.deleted-related-item .icon {
|
|
||||||
color: adjust-hue($activityColorDelete, 60);
|
|
||||||
}
|
|
||||||
&.follow-dataset .icon {
|
|
||||||
color: $activityColorNeutral;
|
|
||||||
}
|
|
||||||
&.follow-user .icon {
|
|
||||||
color: adjust-hue($activityColorNeutral, 20);
|
|
||||||
}
|
|
||||||
&.new-related-item .icon {
|
|
||||||
color: adjust-hue($activityColorNew, -60);
|
|
||||||
}
|
|
||||||
&.follow-group .icon {
|
|
||||||
color: adjust-hue($activityColorNew, -50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-time {
|
|
||||||
width: 250px;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
br.line-height2 {
|
|
||||||
line-height: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pull-right {
|
|
||||||
float: right;
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
/* User Dashboard
|
|
||||||
* Handles the filter dropdown menu and the reduction of the notifications number
|
|
||||||
* within the header to zero
|
|
||||||
*
|
|
||||||
* Examples
|
|
||||||
*
|
|
||||||
* <div data-module="dashboard"></div>
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
this.ckan.module('dashboard', function ($) {
|
|
||||||
return {
|
|
||||||
button: null,
|
|
||||||
popover: null,
|
|
||||||
searchTimeout: null,
|
|
||||||
|
|
||||||
/* Initialises the module setting up elements and event listeners.
|
|
||||||
*
|
|
||||||
* Returns nothing.
|
|
||||||
*/
|
|
||||||
initialize: function () {
|
|
||||||
$.proxyAll(this, /_on/);
|
|
||||||
this.button = $('#followee-filter .btn').
|
|
||||||
on('click', this._onShowFolloweeDropdown);
|
|
||||||
var title = this.button.prop('title');
|
|
||||||
|
|
||||||
this.button.popover = new bootstrap.Popover(document.querySelector('#followee-filter .btn'), {
|
|
||||||
placement: 'bottom',
|
|
||||||
html: true,
|
|
||||||
template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body followee-container"></div></div>',
|
|
||||||
customClass: 'popover-followee',
|
|
||||||
sanitizeFn: function (content) {
|
|
||||||
return DOMPurify.sanitize(content, { ALLOWED_TAGS: [
|
|
||||||
"form", "div", "input", "footer", "header", "h1", "h2", "h3", "h4",
|
|
||||||
"small", "span", "strong", "i", 'a', 'li', 'ul','p'
|
|
||||||
|
|
||||||
]});
|
|
||||||
},
|
|
||||||
content: $('#followee-content').html()
|
|
||||||
});
|
|
||||||
this.button.prop('title', title);
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Handles click event on the 'show me:' dropdown button
|
|
||||||
*
|
|
||||||
* Returns nothing.
|
|
||||||
*/
|
|
||||||
_onShowFolloweeDropdown: function() {
|
|
||||||
this.button.toggleClass('active');
|
|
||||||
if (this.button.hasClass('active')) {
|
|
||||||
setTimeout(this._onInitSearch, 100);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Handles focusing on the input and making sure that the keyup
|
|
||||||
* even is applied to the input
|
|
||||||
*
|
|
||||||
* Returns nothing.
|
|
||||||
*/
|
|
||||||
_onInitSearch: function() {
|
|
||||||
var input = $('input', this.popover);
|
|
||||||
if (!input.hasClass('inited')) {
|
|
||||||
input.
|
|
||||||
on('keyup', this._onSearchKeyUp).
|
|
||||||
addClass('inited');
|
|
||||||
}
|
|
||||||
input.focus();
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Handles the keyup event
|
|
||||||
*
|
|
||||||
* Returns nothing.
|
|
||||||
*/
|
|
||||||
_onSearchKeyUp: function() {
|
|
||||||
clearTimeout(this.searchTimeout);
|
|
||||||
this.searchTimeout = setTimeout(this._onSearchKeyUpTimeout, 300);
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Handles the actual filtering of search results
|
|
||||||
*
|
|
||||||
* Returns nothing.
|
|
||||||
*/
|
|
||||||
_onSearchKeyUpTimeout: function() {
|
|
||||||
this.popover = this.button.popover.tip;
|
|
||||||
var input = $('input', this.popover);
|
|
||||||
var q = input.val().toLowerCase();
|
|
||||||
if (q) {
|
|
||||||
$('li', this.popover).hide();
|
|
||||||
$('li.everything, [data-search^="' + q + '"]', this.popover).show();
|
|
||||||
} else {
|
|
||||||
$('li', this.popover).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
|
@ -1,64 +0,0 @@
|
||||||
describe('ckan.modules.DashboardModule()', function () {
|
|
||||||
before(() => {
|
|
||||||
cy.visit('/');
|
|
||||||
cy.window().then(win => {
|
|
||||||
cy.wrap(win.ckan.module.registry['dashboard']).as('dashboard');
|
|
||||||
win.jQuery('<div id="fixture">').appendTo(win.document.body)
|
|
||||||
cy.loadFixture('dashboard.html').then((template) => {
|
|
||||||
cy.wrap(template).as('template');
|
|
||||||
});
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
cy.window().then(win => {
|
|
||||||
win.jQuery('#fixture').html(this.template);
|
|
||||||
this.el = document.createElement('button');
|
|
||||||
this.sandbox = win.ckan.sandbox();
|
|
||||||
this.sandbox.body = win.jQuery('#fixture');
|
|
||||||
cy.wrap(this.sandbox.body).as('fixture');
|
|
||||||
this.module = new this.dashboard(this.el, {}, this.sandbox);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
//this.fixture.empty();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('.initialize()', function () {
|
|
||||||
it('should bind callback methods to the module', function () {
|
|
||||||
cy.window().then(win => {
|
|
||||||
let target = cy.stub(win.jQuery, 'proxyAll');
|
|
||||||
|
|
||||||
this.module.initialize();
|
|
||||||
|
|
||||||
expect(target).to.be.called;
|
|
||||||
expect(target).to.be.calledWith(this.module, /_on/);
|
|
||||||
|
|
||||||
target.restore();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('.show()', function () {
|
|
||||||
it('should append the popover to the document body', function () {
|
|
||||||
this.module.initialize();
|
|
||||||
this.module.button.click();
|
|
||||||
assert.equal(this.fixture.children().length, 1);
|
|
||||||
assert.equal(this.fixture.find('#followee-filter').length, 1);
|
|
||||||
assert.equal(this.fixture.find('#followee-filter .input-group input').length, 1);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
describe(".search", function(){
|
|
||||||
it('should filter based on query', function() {
|
|
||||||
this.module.initialize();
|
|
||||||
this.module.button.click();
|
|
||||||
cy.get('#fixture #followee-filter #followee-content').invoke('removeAttr', 'style');
|
|
||||||
cy.get('#fixture #followee-filter .nav li').should('have.length', 3);
|
|
||||||
cy.get('#fixture #followee-filter .input-group input.inited').type('text');
|
|
||||||
cy.get('#fixture #followee-filter .nav li[data-search="not valid"]').should('be.visible');
|
|
||||||
cy.get('#fixture #followee-filter .nav li[data-search="test followee"]').should('be.visible');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,14 +0,0 @@
|
||||||
activity:
|
|
||||||
filters: rjsmin
|
|
||||||
output: activity/%(version)s_activity.js
|
|
||||||
extra:
|
|
||||||
preload:
|
|
||||||
- base/main
|
|
||||||
contents:
|
|
||||||
- dashboard.js
|
|
||||||
- activity-stream.js
|
|
||||||
|
|
||||||
activity-css:
|
|
||||||
output: ckanext-activity/%(version)s_activity.css
|
|
||||||
contents:
|
|
||||||
- activity.css
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,278 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
"""
|
|
||||||
Code for generating email notifications for users (e.g. email notifications for
|
|
||||||
new activities in your dashboard activity stream) and emailing them to the
|
|
||||||
users.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import re
|
|
||||||
from typing import Any, cast
|
|
||||||
from jinja2 import Environment
|
|
||||||
|
|
||||||
import ckan.model as model
|
|
||||||
import ckan.logic as logic
|
|
||||||
import ckan.lib.jinja_extensions as jinja_extensions
|
|
||||||
|
|
||||||
from ckan.common import ungettext, ugettext, config
|
|
||||||
from ckan.types import Context
|
|
||||||
|
|
||||||
|
|
||||||
def string_to_timedelta(s: str) -> datetime.timedelta:
|
|
||||||
"""Parse a string s and return a standard datetime.timedelta object.
|
|
||||||
|
|
||||||
Handles days, hours, minutes, seconds, and microseconds.
|
|
||||||
|
|
||||||
Accepts strings in these formats:
|
|
||||||
|
|
||||||
2 days
|
|
||||||
14 days
|
|
||||||
4:35:00 (hours, minutes and seconds)
|
|
||||||
4:35:12.087465 (hours, minutes, seconds and microseconds)
|
|
||||||
7 days, 3:23:34
|
|
||||||
7 days, 3:23:34.087465
|
|
||||||
.087465 (microseconds only)
|
|
||||||
|
|
||||||
:raises ckan.logic.ValidationError: if the given string does not match any
|
|
||||||
of the recognised formats
|
|
||||||
|
|
||||||
"""
|
|
||||||
patterns = []
|
|
||||||
days_only_pattern = r"(?P<days>\d+)\s+day(s)?"
|
|
||||||
patterns.append(days_only_pattern)
|
|
||||||
hms_only_pattern = r"(?P<hours>\d?\d):(?P<minutes>\d\d):(?P<seconds>\d\d)"
|
|
||||||
patterns.append(hms_only_pattern)
|
|
||||||
ms_only_pattern = r".(?P<milliseconds>\d\d\d)(?P<microseconds>\d\d\d)"
|
|
||||||
patterns.append(ms_only_pattern)
|
|
||||||
hms_and_ms_pattern = hms_only_pattern + ms_only_pattern
|
|
||||||
patterns.append(hms_and_ms_pattern)
|
|
||||||
days_and_hms_pattern = r"{0},\s+{1}".format(
|
|
||||||
days_only_pattern, hms_only_pattern
|
|
||||||
)
|
|
||||||
patterns.append(days_and_hms_pattern)
|
|
||||||
days_and_hms_and_ms_pattern = days_and_hms_pattern + ms_only_pattern
|
|
||||||
patterns.append(days_and_hms_and_ms_pattern)
|
|
||||||
|
|
||||||
match = None
|
|
||||||
for pattern in patterns:
|
|
||||||
match = re.match("^{0}$".format(pattern), s)
|
|
||||||
if match:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
raise logic.ValidationError(
|
|
||||||
{"message": "Not a valid time: {0}".format(s)}
|
|
||||||
)
|
|
||||||
|
|
||||||
gd = match.groupdict()
|
|
||||||
days = int(gd.get("days", "0"))
|
|
||||||
hours = int(gd.get("hours", "0"))
|
|
||||||
minutes = int(gd.get("minutes", "0"))
|
|
||||||
seconds = int(gd.get("seconds", "0"))
|
|
||||||
milliseconds = int(gd.get("milliseconds", "0"))
|
|
||||||
microseconds = int(gd.get("microseconds", "0"))
|
|
||||||
delta = datetime.timedelta(
|
|
||||||
days=days,
|
|
||||||
hours=hours,
|
|
||||||
minutes=minutes,
|
|
||||||
seconds=seconds,
|
|
||||||
milliseconds=milliseconds,
|
|
||||||
microseconds=microseconds,
|
|
||||||
)
|
|
||||||
return delta
|
|
||||||
|
|
||||||
|
|
||||||
def render_activity_email(activities: list[dict[str, Any]]) -> str:
|
|
||||||
globals = {"site_title": config.get("ckan.site_title")}
|
|
||||||
template_name = "activity_streams/activity_stream_email_notifications.text"
|
|
||||||
|
|
||||||
env = Environment(**jinja_extensions.get_jinja_env_options())
|
|
||||||
# Install the given gettext, ngettext callables into the environment
|
|
||||||
env.install_gettext_callables(ugettext, ungettext) # type: ignore
|
|
||||||
|
|
||||||
template = env.get_template(template_name, globals=globals)
|
|
||||||
return template.render({"activities": activities})
|
|
||||||
|
|
||||||
|
|
||||||
def _notifications_for_activities(
|
|
||||||
activities: list[dict[str, Any]], user_dict: dict[str, Any]
|
|
||||||
) -> list[dict[str, str]]:
|
|
||||||
"""Return one or more email notifications covering the given activities.
|
|
||||||
|
|
||||||
This function handles grouping multiple activities into a single digest
|
|
||||||
email.
|
|
||||||
|
|
||||||
:param activities: the activities to consider
|
|
||||||
:type activities: list of activity dicts like those returned by
|
|
||||||
ckan.logic.action.get.dashboard_activity_list()
|
|
||||||
|
|
||||||
:returns: a list of email notifications
|
|
||||||
:rtype: list of dicts each with keys 'subject' and 'body'
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not activities:
|
|
||||||
return []
|
|
||||||
|
|
||||||
if not user_dict.get("activity_streams_email_notifications"):
|
|
||||||
return []
|
|
||||||
|
|
||||||
# We just group all activities into a single "new activity" email that
|
|
||||||
# doesn't say anything about _what_ new activities they are.
|
|
||||||
# TODO: Here we could generate some smarter content for the emails e.g.
|
|
||||||
# say something about the contents of the activities, or single out
|
|
||||||
# certain types of activity to be sent in their own individual emails,
|
|
||||||
# etc.
|
|
||||||
|
|
||||||
subject = ungettext(
|
|
||||||
"{n} new activity from {site_title}",
|
|
||||||
"{n} new activities from {site_title}",
|
|
||||||
len(activities),
|
|
||||||
).format(site_title=config.get("ckan.site_title"), n=len(activities))
|
|
||||||
|
|
||||||
body = render_activity_email(activities)
|
|
||||||
notifications = [{"subject": subject, "body": body}]
|
|
||||||
|
|
||||||
return notifications
|
|
||||||
|
|
||||||
|
|
||||||
def _notifications_from_dashboard_activity_list(
|
|
||||||
user_dict: dict[str, Any], since: datetime.datetime
|
|
||||||
) -> list[dict[str, str]]:
|
|
||||||
"""Return any email notifications from the given user's dashboard activity
|
|
||||||
list since `since`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Get the user's dashboard activity stream.
|
|
||||||
context = cast(
|
|
||||||
Context,
|
|
||||||
{"model": model, "session": model.Session, "user": user_dict["id"]},
|
|
||||||
)
|
|
||||||
activity_list = logic.get_action("dashboard_activity_list")(context, {})
|
|
||||||
|
|
||||||
# Filter out the user's own activities., so they don't get an email every
|
|
||||||
# time they themselves do something (we are not Trac).
|
|
||||||
activity_list = [
|
|
||||||
activity
|
|
||||||
for activity in activity_list
|
|
||||||
if activity["user_id"] != user_dict["id"]
|
|
||||||
]
|
|
||||||
|
|
||||||
# Filter out the old activities.
|
|
||||||
strptime = datetime.datetime.strptime
|
|
||||||
fmt = "%Y-%m-%dT%H:%M:%S.%f"
|
|
||||||
activity_list = [
|
|
||||||
activity
|
|
||||||
for activity in activity_list
|
|
||||||
if strptime(activity["timestamp"], fmt) > since
|
|
||||||
]
|
|
||||||
|
|
||||||
return _notifications_for_activities(activity_list, user_dict)
|
|
||||||
|
|
||||||
|
|
||||||
# A list of functions that provide email notifications for users from different
|
|
||||||
# sources. Add to this list if you want to implement a new source of email
|
|
||||||
# notifications.
|
|
||||||
_notifications_functions = [
|
|
||||||
_notifications_from_dashboard_activity_list,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_notifications(
|
|
||||||
user_dict: dict[str, Any], since: datetime.datetime
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Return any email notifications for the given user since `since`.
|
|
||||||
|
|
||||||
For example email notifications about activity streams will be returned for
|
|
||||||
any activities the occurred since `since`.
|
|
||||||
|
|
||||||
:param user_dict: a dictionary representing the user, should contain 'id'
|
|
||||||
and 'name'
|
|
||||||
:type user_dict: dictionary
|
|
||||||
|
|
||||||
:param since: datetime after which to return notifications from
|
|
||||||
:rtype since: datetime.datetime
|
|
||||||
|
|
||||||
:returns: a list of email notifications
|
|
||||||
:rtype: list of dicts with keys 'subject' and 'body'
|
|
||||||
|
|
||||||
"""
|
|
||||||
notifications = []
|
|
||||||
for function in _notifications_functions:
|
|
||||||
notifications.extend(function(user_dict, since))
|
|
||||||
return notifications
|
|
||||||
|
|
||||||
|
|
||||||
def send_notification(
|
|
||||||
user: dict[str, Any], email_dict: dict[str, Any]
|
|
||||||
) -> None:
|
|
||||||
"""Email `email_dict` to `user`."""
|
|
||||||
import ckan.lib.mailer
|
|
||||||
|
|
||||||
if not user.get("email"):
|
|
||||||
# FIXME: Raise an exception.
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
ckan.lib.mailer.mail_recipient(
|
|
||||||
user["display_name"],
|
|
||||||
user["email"],
|
|
||||||
email_dict["subject"],
|
|
||||||
email_dict["body"],
|
|
||||||
)
|
|
||||||
except ckan.lib.mailer.MailerException:
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def get_and_send_notifications_for_user(user: dict[str, Any]) -> None:
|
|
||||||
|
|
||||||
# Parse the email_notifications_since config setting, email notifications
|
|
||||||
# from longer ago than this time will not be sent.
|
|
||||||
email_notifications_since = config.get(
|
|
||||||
"ckan.email_notifications_since"
|
|
||||||
)
|
|
||||||
email_notifications_since = string_to_timedelta(email_notifications_since)
|
|
||||||
email_notifications_since = (
|
|
||||||
datetime.datetime.utcnow() - email_notifications_since
|
|
||||||
)
|
|
||||||
|
|
||||||
# FIXME: We are accessing model from lib here but I'm not sure what
|
|
||||||
# else to do unless we add a get_email_last_sent() logic function which
|
|
||||||
# would only be needed by this lib.
|
|
||||||
dashboard = model.Dashboard.get(user["id"])
|
|
||||||
if dashboard:
|
|
||||||
email_last_sent = dashboard.email_last_sent
|
|
||||||
activity_stream_last_viewed = dashboard.activity_stream_last_viewed
|
|
||||||
since = max(
|
|
||||||
email_notifications_since,
|
|
||||||
email_last_sent,
|
|
||||||
activity_stream_last_viewed,
|
|
||||||
)
|
|
||||||
|
|
||||||
notifications = get_notifications(user, since)
|
|
||||||
# TODO: Handle failures from send_email_notification.
|
|
||||||
for notification in notifications:
|
|
||||||
send_notification(user, notification)
|
|
||||||
|
|
||||||
# FIXME: We are accessing model from lib here but I'm not sure what
|
|
||||||
# else to do unless we add a update_email_last_sent()
|
|
||||||
# logic function which would only be needed by this lib.
|
|
||||||
dashboard.email_last_sent = datetime.datetime.utcnow()
|
|
||||||
model.repo.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def get_and_send_notifications_for_all_users() -> None:
|
|
||||||
context = cast(
|
|
||||||
Context,
|
|
||||||
{
|
|
||||||
"model": model,
|
|
||||||
"session": model.Session,
|
|
||||||
"ignore_auth": True,
|
|
||||||
"keep_email": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
users = logic.get_action("user_list")(context, {})
|
|
||||||
for user in users:
|
|
||||||
get_and_send_notifications_for_user(user)
|
|
|
@ -1,190 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Any, Optional, cast
|
|
||||||
|
|
||||||
import jinja2
|
|
||||||
import datetime
|
|
||||||
from markupsafe import Markup
|
|
||||||
|
|
||||||
import ckan.model as model
|
|
||||||
import ckan.plugins.toolkit as tk
|
|
||||||
|
|
||||||
from ckan.types import Context
|
|
||||||
from . import changes
|
|
||||||
|
|
||||||
|
|
||||||
def dashboard_activity_stream(
|
|
||||||
user_id: str,
|
|
||||||
filter_type: Optional[str] = None,
|
|
||||||
filter_id: Optional[str] = None,
|
|
||||||
offset: int = 0,
|
|
||||||
limit: int = 0,
|
|
||||||
before: Optional[datetime.datetime] = None,
|
|
||||||
after: Optional[datetime.datetime] = None,
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Return the dashboard activity stream of the current user.
|
|
||||||
|
|
||||||
:param user_id: the id of the user
|
|
||||||
:type user_id: string
|
|
||||||
|
|
||||||
:param filter_type: the type of thing to filter by
|
|
||||||
:type filter_type: string
|
|
||||||
|
|
||||||
:param filter_id: the id of item to filter by
|
|
||||||
:type filter_id: string
|
|
||||||
|
|
||||||
:returns: an activity stream as an HTML snippet
|
|
||||||
:rtype: string
|
|
||||||
|
|
||||||
"""
|
|
||||||
context = cast(Context, {"user": tk.g.user})
|
|
||||||
if filter_type:
|
|
||||||
action_functions = {
|
|
||||||
"dataset": "package_activity_list",
|
|
||||||
"user": "user_activity_list",
|
|
||||||
"group": "group_activity_list",
|
|
||||||
"organization": "organization_activity_list",
|
|
||||||
}
|
|
||||||
action_function = tk.get_action(action_functions[filter_type])
|
|
||||||
return action_function(
|
|
||||||
context, {
|
|
||||||
"id": filter_id,
|
|
||||||
"limit": limit,
|
|
||||||
"offset": offset,
|
|
||||||
"before": before,
|
|
||||||
"after": after
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
return tk.get_action("dashboard_activity_list")(
|
|
||||||
context, {
|
|
||||||
"offset": offset,
|
|
||||||
"limit": limit,
|
|
||||||
"before": before,
|
|
||||||
"after": after
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def recently_changed_packages_activity_stream(
|
|
||||||
limit: Optional[int] = None,
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
if limit:
|
|
||||||
data_dict = {"limit": limit}
|
|
||||||
else:
|
|
||||||
data_dict = {}
|
|
||||||
context = cast(
|
|
||||||
Context, {"model": model, "session": model.Session, "user": tk.g.user}
|
|
||||||
)
|
|
||||||
return tk.get_action("recently_changed_packages_activity_list")(
|
|
||||||
context, data_dict
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def new_activities() -> Optional[int]:
|
|
||||||
"""Return the number of activities for the current user.
|
|
||||||
|
|
||||||
See :func:`logic.action.get.dashboard_new_activities_count` for more
|
|
||||||
details.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not tk.g.userobj:
|
|
||||||
return None
|
|
||||||
action = tk.get_action("dashboard_new_activities_count")
|
|
||||||
return action({}, {})
|
|
||||||
|
|
||||||
|
|
||||||
def activity_list_select(
|
|
||||||
pkg_activity_list: list[dict[str, Any]], current_activity_id: str
|
|
||||||
) -> list[Markup]:
|
|
||||||
"""
|
|
||||||
Builds an HTML formatted list of options for the select lists
|
|
||||||
on the "Changes" summary page.
|
|
||||||
"""
|
|
||||||
select_list = []
|
|
||||||
template = jinja2.Template(
|
|
||||||
'<option value="{{activity_id}}" {{selected}}>{{timestamp}}</option>',
|
|
||||||
autoescape=True,
|
|
||||||
)
|
|
||||||
for activity in pkg_activity_list:
|
|
||||||
entry = tk.h.render_datetime(
|
|
||||||
activity["timestamp"], with_hours=True, with_seconds=True
|
|
||||||
)
|
|
||||||
select_list.append(
|
|
||||||
Markup(
|
|
||||||
template.render(
|
|
||||||
activity_id=activity["id"],
|
|
||||||
timestamp=entry,
|
|
||||||
selected="selected"
|
|
||||||
if activity["id"] == current_activity_id
|
|
||||||
else "",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return select_list
|
|
||||||
|
|
||||||
|
|
||||||
def compare_pkg_dicts(
|
|
||||||
old: dict[str, Any], new: dict[str, Any], old_activity_id: str
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Takes two package dictionaries that represent consecutive versions of
|
|
||||||
the same dataset and returns a list of detailed & formatted summaries of
|
|
||||||
the changes between the two versions. old and new are the two package
|
|
||||||
dictionaries. The function assumes that both dictionaries will have
|
|
||||||
all of the default package dictionary keys, and also checks for fields
|
|
||||||
added by extensions and extra fields added by the user in the web
|
|
||||||
interface.
|
|
||||||
|
|
||||||
Returns a list of dictionaries, each of which corresponds to a change
|
|
||||||
to the dataset made in this revision. The dictionaries each contain a
|
|
||||||
string indicating the type of change made as well as other data necessary
|
|
||||||
to form a detailed summary of the change.
|
|
||||||
"""
|
|
||||||
|
|
||||||
change_list: list[dict[str, Any]] = []
|
|
||||||
|
|
||||||
changes.check_metadata_changes(change_list, old, new)
|
|
||||||
|
|
||||||
changes.check_resource_changes(change_list, old, new, old_activity_id)
|
|
||||||
|
|
||||||
# if the dataset was updated but none of the fields we check were changed,
|
|
||||||
# display a message stating that
|
|
||||||
if len(change_list) == 0:
|
|
||||||
change_list.append({"type": "no_change"})
|
|
||||||
|
|
||||||
return change_list
|
|
||||||
|
|
||||||
|
|
||||||
def compare_group_dicts(
|
|
||||||
old: dict[str, Any], new: dict[str, Any], old_activity_id: str
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Takes two package dictionaries that represent consecutive versions of
|
|
||||||
the same organization and returns a list of detailed & formatted summaries
|
|
||||||
of the changes between the two versions. old and new are the two package
|
|
||||||
dictionaries. The function assumes that both dictionaries will have
|
|
||||||
all of the default package dictionary keys, and also checks for fields
|
|
||||||
added by extensions and extra fields added by the user in the web
|
|
||||||
interface.
|
|
||||||
|
|
||||||
Returns a list of dictionaries, each of which corresponds to a change
|
|
||||||
to the dataset made in this revision. The dictionaries each contain a
|
|
||||||
string indicating the type of change made as well as other data necessary
|
|
||||||
to form a detailed summary of the change.
|
|
||||||
"""
|
|
||||||
change_list: list[dict[str, Any]] = []
|
|
||||||
|
|
||||||
changes.check_metadata_org_changes(change_list, old, new)
|
|
||||||
|
|
||||||
# if the organization was updated but none of the fields we check
|
|
||||||
# were changed, display a message stating that
|
|
||||||
if len(change_list) == 0:
|
|
||||||
change_list.append({"type": "no_change"})
|
|
||||||
|
|
||||||
return change_list
|
|
||||||
|
|
||||||
|
|
||||||
def activity_show_email_notifications() -> bool:
|
|
||||||
return tk.config.get("ckan.activity_streams_email_notifications")
|
|
|
@ -1,647 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import datetime
|
|
||||||
import json
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
import ckan.plugins.toolkit as tk
|
|
||||||
|
|
||||||
from ckan.logic import validate
|
|
||||||
from ckan.types import Context, DataDict, ActionResult
|
|
||||||
import ckanext.activity.email_notifications as email_notifications
|
|
||||||
|
|
||||||
from . import schema
|
|
||||||
from ..model import activity as model_activity, activity_dict_save
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def send_email_notifications(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> ActionResult.SendEmailNotifications:
|
|
||||||
"""Send any pending activity stream notification emails to users.
|
|
||||||
|
|
||||||
You must provide a sysadmin's API key/token in the Authorization header of
|
|
||||||
the request, or call this action from the command-line via a `ckan notify
|
|
||||||
send_emails ...` command.
|
|
||||||
|
|
||||||
"""
|
|
||||||
tk.check_access("send_email_notifications", context, data_dict)
|
|
||||||
|
|
||||||
if not tk.config.get("ckan.activity_streams_email_notifications"):
|
|
||||||
raise tk.ValidationError(
|
|
||||||
{
|
|
||||||
"message": (
|
|
||||||
"ckan.activity_streams_email_notifications"
|
|
||||||
" is not enabled in config"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
email_notifications.get_and_send_notifications_for_all_users()
|
|
||||||
|
|
||||||
|
|
||||||
def dashboard_mark_activities_old(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> ActionResult.DashboardMarkActivitiesOld:
|
|
||||||
"""Mark all the authorized user's new dashboard activities as old.
|
|
||||||
|
|
||||||
This will reset
|
|
||||||
:py:func:`~ckan.logic.action.get.dashboard_new_activities_count` to 0.
|
|
||||||
|
|
||||||
"""
|
|
||||||
tk.check_access("dashboard_mark_activities_old", context, data_dict)
|
|
||||||
model = context["model"]
|
|
||||||
user_obj = model.User.get(context["user"])
|
|
||||||
assert user_obj
|
|
||||||
user_id = user_obj.id
|
|
||||||
dashboard = model.Dashboard.get(user_id)
|
|
||||||
if dashboard:
|
|
||||||
dashboard.activity_stream_last_viewed = datetime.datetime.utcnow()
|
|
||||||
if not context.get("defer_commit"):
|
|
||||||
model.repo.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def activity_create(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> Optional[dict[str, Any]]:
|
|
||||||
"""Create a new activity stream activity.
|
|
||||||
|
|
||||||
You must be a sysadmin to create new activities.
|
|
||||||
|
|
||||||
:param user_id: the name or id of the user who carried out the activity,
|
|
||||||
e.g. ``'seanh'``
|
|
||||||
:type user_id: string
|
|
||||||
:param object_id: the name or id of the object of the activity, e.g.
|
|
||||||
``'my_dataset'``
|
|
||||||
:param activity_type: the type of the activity, this must be an activity
|
|
||||||
type that CKAN knows how to render, e.g. ``'new package'``,
|
|
||||||
``'changed user'``, ``'deleted group'`` etc.
|
|
||||||
:type activity_type: string
|
|
||||||
:param data: any additional data about the activity
|
|
||||||
:type data: dictionary
|
|
||||||
|
|
||||||
:returns: the newly created activity
|
|
||||||
:rtype: dictionary
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
tk.check_access("activity_create", context, data_dict)
|
|
||||||
|
|
||||||
if not tk.config.get("ckan.activity_streams_enabled"):
|
|
||||||
return
|
|
||||||
|
|
||||||
model = context["model"]
|
|
||||||
|
|
||||||
# Any revision_id that the caller attempts to pass in the activity_dict is
|
|
||||||
# ignored and removed here.
|
|
||||||
if "revision_id" in data_dict:
|
|
||||||
del data_dict["revision_id"]
|
|
||||||
|
|
||||||
sch = context.get("schema") or schema.default_create_activity_schema()
|
|
||||||
|
|
||||||
data, errors = tk.navl_validate(data_dict, sch, context)
|
|
||||||
if errors:
|
|
||||||
raise tk.ValidationError(errors)
|
|
||||||
|
|
||||||
activity = activity_dict_save(data, context)
|
|
||||||
|
|
||||||
if not context.get("defer_commit"):
|
|
||||||
model.repo.commit()
|
|
||||||
|
|
||||||
log.debug("Created '%s' activity" % activity.activity_type)
|
|
||||||
return model_activity.activity_dictize(activity, context)
|
|
||||||
|
|
||||||
|
|
||||||
@validate(schema.default_activity_list_schema)
|
|
||||||
@tk.side_effect_free
|
|
||||||
def user_activity_list(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Return a user's public activity stream.
|
|
||||||
|
|
||||||
You must be authorized to view the user's profile.
|
|
||||||
|
|
||||||
|
|
||||||
:param id: the id or name of the user
|
|
||||||
:type id: string
|
|
||||||
:param offset: where to start getting activity items from
|
|
||||||
(optional, default: ``0``)
|
|
||||||
:type offset: int
|
|
||||||
:param limit: the maximum number of activities to return
|
|
||||||
(optional, default: ``31`` unless set in site's configuration
|
|
||||||
``ckan.activity_list_limit``, upper limit: ``100`` unless set in
|
|
||||||
site's configuration ``ckan.activity_list_limit_max``)
|
|
||||||
:type limit: int
|
|
||||||
:param after: After timestamp
|
|
||||||
(optional, default: ``None``)
|
|
||||||
:type after: int, str
|
|
||||||
:param before: Before timestamp
|
|
||||||
(optional, default: ``None``)
|
|
||||||
:type before: int, str
|
|
||||||
|
|
||||||
:rtype: list of dictionaries
|
|
||||||
|
|
||||||
"""
|
|
||||||
# FIXME: Filter out activities whose subject or object the user is not
|
|
||||||
# authorized to read.
|
|
||||||
tk.check_access("user_activity_list", context, data_dict)
|
|
||||||
|
|
||||||
model = context["model"]
|
|
||||||
|
|
||||||
user_ref = data_dict.get("id") # May be user name or id.
|
|
||||||
user = model.User.get(user_ref)
|
|
||||||
if user is None:
|
|
||||||
raise tk.ObjectNotFound()
|
|
||||||
|
|
||||||
offset = data_dict.get("offset", 0)
|
|
||||||
limit = data_dict["limit"] # defaulted, limited & made an int by schema
|
|
||||||
after = data_dict.get("after")
|
|
||||||
before = data_dict.get("before")
|
|
||||||
|
|
||||||
activity_objects = model_activity.user_activity_list(
|
|
||||||
user.id,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset,
|
|
||||||
after=after,
|
|
||||||
before=before,
|
|
||||||
)
|
|
||||||
|
|
||||||
return model_activity.activity_list_dictize(activity_objects, context)
|
|
||||||
|
|
||||||
|
|
||||||
@validate(schema.default_activity_list_schema)
|
|
||||||
@tk.side_effect_free
|
|
||||||
def package_activity_list(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Return a package's activity stream (not including detail)
|
|
||||||
|
|
||||||
You must be authorized to view the package.
|
|
||||||
|
|
||||||
:param id: the id or name of the package
|
|
||||||
:type id: string
|
|
||||||
:param offset: where to start getting activity items from
|
|
||||||
(optional, default: ``0``)
|
|
||||||
:type offset: int
|
|
||||||
:param limit: the maximum number of activities to return
|
|
||||||
(optional, default: ``31`` unless set in site's configuration
|
|
||||||
``ckan.activity_list_limit``, upper limit: ``100`` unless set in
|
|
||||||
site's configuration ``ckan.activity_list_limit_max``)
|
|
||||||
:type limit: int
|
|
||||||
:param after: After timestamp
|
|
||||||
(optional, default: ``None``)
|
|
||||||
:type after: int, str
|
|
||||||
:param before: Before timestamp
|
|
||||||
(optional, default: ``None``)
|
|
||||||
:type before: int, str
|
|
||||||
:param include_hidden_activity: whether to include 'hidden' activity, which
|
|
||||||
is not shown in the Activity Stream page. Hidden activity includes
|
|
||||||
activity done by the site_user, such as harvests, which are not shown
|
|
||||||
in the activity stream because they can be too numerous, or activity by
|
|
||||||
other users specified in config option `ckan.hide_activity_from_users`.
|
|
||||||
NB Only sysadmins may set include_hidden_activity to true.
|
|
||||||
(default: false)
|
|
||||||
:type include_hidden_activity: bool
|
|
||||||
:param activity_types: A list of activity types to include in the response
|
|
||||||
:type activity_types: list
|
|
||||||
|
|
||||||
:param exclude_activity_types: A list of activity types to exclude from the
|
|
||||||
response
|
|
||||||
:type exclude_activity_types: list
|
|
||||||
|
|
||||||
:rtype: list of dictionaries
|
|
||||||
|
|
||||||
"""
|
|
||||||
# FIXME: Filter out activities whose subject or object the user is not
|
|
||||||
# authorized to read.
|
|
||||||
include_hidden_activity = data_dict.get("include_hidden_activity", False)
|
|
||||||
activity_types = data_dict.pop("activity_types", None)
|
|
||||||
exclude_activity_types = data_dict.pop("exclude_activity_types", None)
|
|
||||||
|
|
||||||
if activity_types is not None and exclude_activity_types is not None:
|
|
||||||
raise tk.ValidationError(
|
|
||||||
{
|
|
||||||
"activity_types": [
|
|
||||||
"Cannot be used together with `exclude_activity_types"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
tk.check_access("package_activity_list", context, data_dict)
|
|
||||||
|
|
||||||
model = context["model"]
|
|
||||||
|
|
||||||
package_ref = data_dict.get("id") # May be name or ID.
|
|
||||||
package = model.Package.get(package_ref)
|
|
||||||
if package is None:
|
|
||||||
raise tk.ObjectNotFound()
|
|
||||||
|
|
||||||
offset = int(data_dict.get("offset", 0))
|
|
||||||
limit = data_dict["limit"] # defaulted, limited & made an int by schema
|
|
||||||
after = data_dict.get("after")
|
|
||||||
before = data_dict.get("before")
|
|
||||||
|
|
||||||
activity_objects = model_activity.package_activity_list(
|
|
||||||
package.id,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset,
|
|
||||||
after=after,
|
|
||||||
before=before,
|
|
||||||
include_hidden_activity=include_hidden_activity,
|
|
||||||
activity_types=activity_types,
|
|
||||||
exclude_activity_types=exclude_activity_types,
|
|
||||||
)
|
|
||||||
|
|
||||||
return model_activity.activity_list_dictize(activity_objects, context)
|
|
||||||
|
|
||||||
|
|
||||||
@validate(schema.default_activity_list_schema)
|
|
||||||
@tk.side_effect_free
|
|
||||||
def group_activity_list(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Return a group's activity stream.
|
|
||||||
|
|
||||||
You must be authorized to view the group.
|
|
||||||
|
|
||||||
:param id: the id or name of the group
|
|
||||||
:type id: string
|
|
||||||
:param offset: where to start getting activity items from
|
|
||||||
(optional, default: ``0``)
|
|
||||||
:type offset: int
|
|
||||||
:param limit: the maximum number of activities to return
|
|
||||||
(optional, default: ``31`` unless set in site's configuration
|
|
||||||
``ckan.activity_list_limit``, upper limit: ``100`` unless set in
|
|
||||||
site's configuration ``ckan.activity_list_limit_max``)
|
|
||||||
:type limit: int
|
|
||||||
:param include_hidden_activity: whether to include 'hidden' activity, which
|
|
||||||
is not shown in the Activity Stream page. Hidden activity includes
|
|
||||||
activity done by the site_user, such as harvests, which are not shown
|
|
||||||
in the activity stream because they can be too numerous, or activity by
|
|
||||||
other users specified in config option `ckan.hide_activity_from_users`.
|
|
||||||
NB Only sysadmins may set include_hidden_activity to true.
|
|
||||||
(default: false)
|
|
||||||
:type include_hidden_activity: bool
|
|
||||||
|
|
||||||
:rtype: list of dictionaries
|
|
||||||
|
|
||||||
"""
|
|
||||||
# FIXME: Filter out activities whose subject or object the user is not
|
|
||||||
# authorized to read.
|
|
||||||
data_dict = dict(data_dict, include_data=False)
|
|
||||||
include_hidden_activity = data_dict.get("include_hidden_activity", False)
|
|
||||||
activity_types = data_dict.pop("activity_types", None)
|
|
||||||
tk.check_access("group_activity_list", context, data_dict)
|
|
||||||
|
|
||||||
group_id = data_dict.get("id")
|
|
||||||
offset = data_dict.get("offset", 0)
|
|
||||||
limit = data_dict["limit"] # defaulted, limited & made an int by schema
|
|
||||||
|
|
||||||
# Convert group_id (could be id or name) into id.
|
|
||||||
group_show = tk.get_action("group_show")
|
|
||||||
group_id = group_show(context, {"id": group_id})["id"]
|
|
||||||
|
|
||||||
after = data_dict.get("after")
|
|
||||||
before = data_dict.get("before")
|
|
||||||
|
|
||||||
activity_objects = model_activity.group_activity_list(
|
|
||||||
group_id,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset,
|
|
||||||
after=after,
|
|
||||||
before=before,
|
|
||||||
include_hidden_activity=include_hidden_activity,
|
|
||||||
activity_types=activity_types
|
|
||||||
)
|
|
||||||
|
|
||||||
return model_activity.activity_list_dictize(activity_objects, context)
|
|
||||||
|
|
||||||
|
|
||||||
@validate(schema.default_activity_list_schema)
|
|
||||||
@tk.side_effect_free
|
|
||||||
def organization_activity_list(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Return a organization's activity stream.
|
|
||||||
|
|
||||||
:param id: the id or name of the organization
|
|
||||||
:type id: string
|
|
||||||
:param offset: where to start getting activity items from
|
|
||||||
(optional, default: ``0``)
|
|
||||||
:type offset: int
|
|
||||||
:param limit: the maximum number of activities to return
|
|
||||||
(optional, default: ``31`` unless set in site's configuration
|
|
||||||
``ckan.activity_list_limit``, upper limit: ``100`` unless set in
|
|
||||||
site's configuration ``ckan.activity_list_limit_max``)
|
|
||||||
:type limit: int
|
|
||||||
:param include_hidden_activity: whether to include 'hidden' activity, which
|
|
||||||
is not shown in the Activity Stream page. Hidden activity includes
|
|
||||||
activity done by the site_user, such as harvests, which are not shown
|
|
||||||
in the activity stream because they can be too numerous, or activity by
|
|
||||||
other users specified in config option `ckan.hide_activity_from_users`.
|
|
||||||
NB Only sysadmins may set include_hidden_activity to true.
|
|
||||||
(default: false)
|
|
||||||
:type include_hidden_activity: bool
|
|
||||||
|
|
||||||
:rtype: list of dictionaries
|
|
||||||
|
|
||||||
"""
|
|
||||||
# FIXME: Filter out activities whose subject or object the user is not
|
|
||||||
# authorized to read.
|
|
||||||
include_hidden_activity = data_dict.get("include_hidden_activity", False)
|
|
||||||
tk.check_access("organization_activity_list", context, data_dict)
|
|
||||||
|
|
||||||
org_id = data_dict.get("id")
|
|
||||||
offset = data_dict.get("offset", 0)
|
|
||||||
limit = data_dict["limit"] # defaulted, limited & made an int by schema
|
|
||||||
activity_types = data_dict.pop("activity_types", None)
|
|
||||||
|
|
||||||
# Convert org_id (could be id or name) into id.
|
|
||||||
org_show = tk.get_action("organization_show")
|
|
||||||
org_id = org_show(context, {"id": org_id})["id"]
|
|
||||||
|
|
||||||
after = data_dict.get("after")
|
|
||||||
before = data_dict.get("before")
|
|
||||||
|
|
||||||
activity_objects = model_activity.organization_activity_list(
|
|
||||||
org_id,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset,
|
|
||||||
after=after,
|
|
||||||
before=before,
|
|
||||||
include_hidden_activity=include_hidden_activity,
|
|
||||||
activity_types=activity_types
|
|
||||||
)
|
|
||||||
|
|
||||||
return model_activity.activity_list_dictize(activity_objects, context)
|
|
||||||
|
|
||||||
|
|
||||||
@validate(schema.default_dashboard_activity_list_schema)
|
|
||||||
@tk.side_effect_free
|
|
||||||
def recently_changed_packages_activity_list(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Return the activity stream of all recently added or changed packages.
|
|
||||||
|
|
||||||
:param offset: where to start getting activity items from
|
|
||||||
(optional, default: ``0``)
|
|
||||||
:type offset: int
|
|
||||||
:param limit: the maximum number of activities to return
|
|
||||||
(optional, default: ``31`` unless set in site's configuration
|
|
||||||
``ckan.activity_list_limit``, upper limit: ``100`` unless set in
|
|
||||||
site's configuration ``ckan.activity_list_limit_max``)
|
|
||||||
:type limit: int
|
|
||||||
|
|
||||||
:rtype: list of dictionaries
|
|
||||||
|
|
||||||
"""
|
|
||||||
# FIXME: Filter out activities whose subject or object the user is not
|
|
||||||
# authorized to read.
|
|
||||||
offset = data_dict.get("offset", 0)
|
|
||||||
limit = data_dict["limit"] # defaulted, limited & made an int by schema
|
|
||||||
|
|
||||||
activity_objects = model_activity.recently_changed_packages_activity_list(
|
|
||||||
limit=limit, offset=offset
|
|
||||||
)
|
|
||||||
|
|
||||||
return model_activity.activity_list_dictize(activity_objects, context)
|
|
||||||
|
|
||||||
|
|
||||||
@validate(schema.default_dashboard_activity_list_schema)
|
|
||||||
@tk.side_effect_free
|
|
||||||
def dashboard_activity_list(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Return the authorized (via login or API key) user's dashboard activity
|
|
||||||
stream.
|
|
||||||
|
|
||||||
Unlike the activity dictionaries returned by other ``*_activity_list``
|
|
||||||
actions, these activity dictionaries have an extra boolean value with key
|
|
||||||
``is_new`` that tells you whether the activity happened since the user last
|
|
||||||
viewed her dashboard (``'is_new': True``) or not (``'is_new': False``).
|
|
||||||
|
|
||||||
The user's own activities are always marked ``'is_new': False``.
|
|
||||||
|
|
||||||
:param offset: where to start getting activity items from
|
|
||||||
(optional, default: ``0``)
|
|
||||||
:type offset: int
|
|
||||||
:param limit: the maximum number of activities to return
|
|
||||||
(optional, default: ``31`` unless set in site's configuration
|
|
||||||
``ckan.activity_list_limit``, upper limit: ``100`` unless set in
|
|
||||||
site's configuration ``ckan.activity_list_limit_max``)
|
|
||||||
:type limit: int
|
|
||||||
|
|
||||||
:rtype: list of activity dictionaries
|
|
||||||
|
|
||||||
"""
|
|
||||||
tk.check_access("dashboard_activity_list", context, data_dict)
|
|
||||||
|
|
||||||
model = context["model"]
|
|
||||||
user_obj = model.User.get(context["user"])
|
|
||||||
assert user_obj
|
|
||||||
user_id = user_obj.id
|
|
||||||
offset = data_dict.get("offset", 0)
|
|
||||||
limit = data_dict["limit"] # defaulted, limited & made an int by schema
|
|
||||||
before = data_dict.get("before")
|
|
||||||
after = data_dict.get("after")
|
|
||||||
# FIXME: Filter out activities whose subject or object the user is not
|
|
||||||
# authorized to read.
|
|
||||||
activity_objects = model_activity.dashboard_activity_list(
|
|
||||||
user_id, limit=limit, offset=offset, before=before, after=after
|
|
||||||
)
|
|
||||||
|
|
||||||
activity_dicts = model_activity.activity_list_dictize(
|
|
||||||
activity_objects, context
|
|
||||||
)
|
|
||||||
|
|
||||||
# Mark the new (not yet seen by user) activities.
|
|
||||||
strptime = datetime.datetime.strptime
|
|
||||||
fmt = "%Y-%m-%dT%H:%M:%S.%f"
|
|
||||||
dashboard = model.Dashboard.get(user_id)
|
|
||||||
last_viewed = None
|
|
||||||
if dashboard:
|
|
||||||
last_viewed = dashboard.activity_stream_last_viewed
|
|
||||||
for activity in activity_dicts:
|
|
||||||
if activity["user_id"] == user_id:
|
|
||||||
# Never mark the user's own activities as new.
|
|
||||||
activity["is_new"] = False
|
|
||||||
elif last_viewed:
|
|
||||||
activity["is_new"] = (
|
|
||||||
strptime(activity["timestamp"], fmt) > last_viewed
|
|
||||||
)
|
|
||||||
|
|
||||||
return activity_dicts
|
|
||||||
|
|
||||||
|
|
||||||
@tk.side_effect_free
|
|
||||||
def dashboard_new_activities_count(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> ActionResult.DashboardNewActivitiesCount:
|
|
||||||
"""Return the number of new activities in the user's dashboard.
|
|
||||||
|
|
||||||
Return the number of new activities in the authorized user's dashboard
|
|
||||||
activity stream.
|
|
||||||
|
|
||||||
Activities from the user herself are not counted by this function even
|
|
||||||
though they appear in the dashboard (users don't want to be notified about
|
|
||||||
things they did themselves).
|
|
||||||
|
|
||||||
:rtype: int
|
|
||||||
|
|
||||||
"""
|
|
||||||
tk.check_access("dashboard_new_activities_count", context, data_dict)
|
|
||||||
activities = tk.get_action("dashboard_activity_list")(context, data_dict)
|
|
||||||
return len([activity for activity in activities if activity["is_new"]])
|
|
||||||
|
|
||||||
|
|
||||||
@tk.side_effect_free
|
|
||||||
def activity_show(context: Context, data_dict: DataDict) -> dict[str, Any]:
|
|
||||||
"""Show details of an item of 'activity' (part of the activity stream).
|
|
||||||
|
|
||||||
:param id: the id of the activity
|
|
||||||
:type id: string
|
|
||||||
|
|
||||||
:rtype: dictionary
|
|
||||||
"""
|
|
||||||
model = context["model"]
|
|
||||||
activity_id = tk.get_or_bust(data_dict, "id")
|
|
||||||
|
|
||||||
activity = model.Session.query(model_activity.Activity).get(activity_id)
|
|
||||||
if activity is None:
|
|
||||||
raise tk.ObjectNotFound()
|
|
||||||
context["activity"] = activity
|
|
||||||
|
|
||||||
tk.check_access("activity_show", context, data_dict)
|
|
||||||
|
|
||||||
activity = model_activity.activity_dictize(activity, context)
|
|
||||||
return activity
|
|
||||||
|
|
||||||
|
|
||||||
@tk.side_effect_free
|
|
||||||
def activity_data_show(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Show the data from an item of 'activity' (part of the activity
|
|
||||||
stream).
|
|
||||||
|
|
||||||
For example for a package update this returns just the dataset dict but
|
|
||||||
none of the activity stream info of who and when the version was created.
|
|
||||||
|
|
||||||
:param id: the id of the activity
|
|
||||||
:type id: string
|
|
||||||
:param object_type: 'package', 'user', 'group' or 'organization'
|
|
||||||
:type object_type: string
|
|
||||||
|
|
||||||
:rtype: dictionary
|
|
||||||
"""
|
|
||||||
model = context["model"]
|
|
||||||
activity_id = tk.get_or_bust(data_dict, "id")
|
|
||||||
object_type = data_dict.get("object_type")
|
|
||||||
|
|
||||||
activity = model.Session.query(model_activity.Activity).get(activity_id)
|
|
||||||
if activity is None:
|
|
||||||
raise tk.ObjectNotFound()
|
|
||||||
context["activity"] = activity
|
|
||||||
|
|
||||||
tk.check_access("activity_data_show", context, data_dict)
|
|
||||||
|
|
||||||
activity = model_activity.activity_dictize(activity, context)
|
|
||||||
try:
|
|
||||||
activity_data = activity["data"]
|
|
||||||
except KeyError:
|
|
||||||
raise tk.ObjectNotFound("Could not find data in the activity")
|
|
||||||
if object_type:
|
|
||||||
try:
|
|
||||||
activity_data = activity_data[object_type]
|
|
||||||
except KeyError:
|
|
||||||
raise tk.ObjectNotFound(
|
|
||||||
"Could not find that object_type in the activity"
|
|
||||||
)
|
|
||||||
return activity_data
|
|
||||||
|
|
||||||
|
|
||||||
@tk.side_effect_free
|
|
||||||
def activity_diff(context: Context, data_dict: DataDict) -> dict[str, Any]:
|
|
||||||
"""Returns a diff of the activity, compared to the previous version of the
|
|
||||||
object
|
|
||||||
|
|
||||||
:param id: the id of the activity
|
|
||||||
:type id: string
|
|
||||||
:param object_type: 'package', 'user', 'group' or 'organization'
|
|
||||||
:type object_type: string
|
|
||||||
:param diff_type: 'unified', 'context', 'html'
|
|
||||||
:type diff_type: string
|
|
||||||
"""
|
|
||||||
import difflib
|
|
||||||
|
|
||||||
model = context["model"]
|
|
||||||
activity_id = tk.get_or_bust(data_dict, "id")
|
|
||||||
object_type = tk.get_or_bust(data_dict, "object_type")
|
|
||||||
diff_type = data_dict.get("diff_type", "unified")
|
|
||||||
|
|
||||||
tk.check_access("activity_diff", context, data_dict)
|
|
||||||
|
|
||||||
activity = model.Session.query(model_activity.Activity).get(activity_id)
|
|
||||||
if activity is None:
|
|
||||||
raise tk.ObjectNotFound()
|
|
||||||
prev_activity = (
|
|
||||||
model.Session.query(model_activity.Activity)
|
|
||||||
.filter_by(object_id=activity.object_id)
|
|
||||||
.filter(model_activity.Activity.timestamp < activity.timestamp)
|
|
||||||
.order_by(
|
|
||||||
# type_ignore_reason: incomplete SQLAlchemy types
|
|
||||||
model_activity.Activity.timestamp.desc() # type: ignore
|
|
||||||
)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if prev_activity is None:
|
|
||||||
raise tk.ObjectNotFound("Previous activity for this object not found")
|
|
||||||
activity_objs = [prev_activity, activity]
|
|
||||||
try:
|
|
||||||
objs = [
|
|
||||||
activity_obj.data[object_type] for activity_obj in activity_objs
|
|
||||||
]
|
|
||||||
except KeyError:
|
|
||||||
raise tk.ObjectNotFound("Could not find object in the activity data")
|
|
||||||
# convert each object dict to 'pprint'-style
|
|
||||||
# and split into lines to suit difflib
|
|
||||||
obj_lines = [
|
|
||||||
json.dumps(obj, indent=2, sort_keys=True).split("\n") for obj in objs
|
|
||||||
]
|
|
||||||
|
|
||||||
# do the diff
|
|
||||||
if diff_type == "unified":
|
|
||||||
# type_ignore_reason: typechecker can't predict number of items
|
|
||||||
diff_generator = difflib.unified_diff(*obj_lines) # type: ignore
|
|
||||||
diff = "\n".join(line for line in diff_generator)
|
|
||||||
elif diff_type == "context":
|
|
||||||
# type_ignore_reason: typechecker can't predict number of items
|
|
||||||
diff_generator = difflib.context_diff(*obj_lines) # type: ignore
|
|
||||||
diff = "\n".join(line for line in diff_generator)
|
|
||||||
elif diff_type == "html":
|
|
||||||
# word-wrap lines. Otherwise you get scroll bars for most datasets.
|
|
||||||
import re
|
|
||||||
|
|
||||||
for obj_index in (0, 1):
|
|
||||||
wrapped_obj_lines = []
|
|
||||||
for line in obj_lines[obj_index]:
|
|
||||||
wrapped_obj_lines.extend(re.findall(r".{1,70}(?:\s+|$)", line))
|
|
||||||
obj_lines[obj_index] = wrapped_obj_lines
|
|
||||||
# type_ignore_reason: typechecker can't predict number of items
|
|
||||||
diff = difflib.HtmlDiff().make_table(*obj_lines) # type: ignore
|
|
||||||
else:
|
|
||||||
raise tk.ValidationError({"message": "diff_type not recognized"})
|
|
||||||
|
|
||||||
activities = [
|
|
||||||
model_activity.activity_dictize(activity_obj, context)
|
|
||||||
for activity_obj in activity_objs
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"diff": diff,
|
|
||||||
"activities": activities,
|
|
||||||
}
|
|
|
@ -1,184 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import ckan.authz as authz
|
|
||||||
import ckan.plugins.toolkit as tk
|
|
||||||
from ckan.types import Context, DataDict, AuthResult
|
|
||||||
|
|
||||||
from ..model import Activity
|
|
||||||
|
|
||||||
|
|
||||||
def _get_activity_object(
|
|
||||||
context: Context, data_dict: Optional[DataDict] = None
|
|
||||||
) -> Activity:
|
|
||||||
try:
|
|
||||||
return context["activity"]
|
|
||||||
except KeyError:
|
|
||||||
if not data_dict:
|
|
||||||
data_dict = {}
|
|
||||||
id = data_dict.get("id", None)
|
|
||||||
if not id:
|
|
||||||
raise tk.ValidationError(
|
|
||||||
{"message": "Missing id, can not get Activity object"}
|
|
||||||
)
|
|
||||||
obj = Activity.get(id)
|
|
||||||
if not obj:
|
|
||||||
raise tk.ObjectNotFound()
|
|
||||||
# Save in case we need this again during the request
|
|
||||||
context["activity"] = obj
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
def send_email_notifications(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> AuthResult:
|
|
||||||
# Only sysadmins are authorized to send email notifications.
|
|
||||||
return {"success": False}
|
|
||||||
|
|
||||||
|
|
||||||
def activity_create(context: Context, data_dict: DataDict) -> AuthResult:
|
|
||||||
return {"success": False}
|
|
||||||
|
|
||||||
|
|
||||||
@tk.auth_allow_anonymous_access
|
|
||||||
def dashboard_activity_list(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> AuthResult:
|
|
||||||
# FIXME: context['user'] could be an IP address but that case is not
|
|
||||||
# handled here. Maybe add an auth helper function like is_logged_in().
|
|
||||||
if context.get("user"):
|
|
||||||
return {"success": True}
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"msg": tk._("You must be logged in to access your dashboard."),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@tk.auth_allow_anonymous_access
|
|
||||||
def dashboard_new_activities_count(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> AuthResult:
|
|
||||||
# FIXME: This should go through check_access() not call is_authorized()
|
|
||||||
# directly, but wait until 2939-orgs is merged before fixing this.
|
|
||||||
# This is so a better not authourized message can be sent.
|
|
||||||
return authz.is_authorized("dashboard_activity_list", context, data_dict)
|
|
||||||
|
|
||||||
|
|
||||||
@tk.auth_allow_anonymous_access
|
|
||||||
def activity_list(context: Context, data_dict: DataDict) -> AuthResult:
|
|
||||||
"""
|
|
||||||
:param id: the id or name of the object (e.g. package id)
|
|
||||||
:type id: string
|
|
||||||
:param object_type: The type of the object (e.g. 'package', 'organization',
|
|
||||||
'group', 'user')
|
|
||||||
:type object_type: string
|
|
||||||
:param include_data: include the data field, containing a full object dict
|
|
||||||
(otherwise the data field is only returned with the object's title)
|
|
||||||
:type include_data: boolean
|
|
||||||
"""
|
|
||||||
if data_dict["object_type"] not in (
|
|
||||||
"package",
|
|
||||||
"organization",
|
|
||||||
"group",
|
|
||||||
"user",
|
|
||||||
):
|
|
||||||
return {"success": False, "msg": "object_type not recognized"}
|
|
||||||
is_public = authz.check_config_permission("public_activity_stream_detail")
|
|
||||||
if data_dict.get("include_data") and not is_public:
|
|
||||||
# The 'data' field of the activity is restricted to users who are
|
|
||||||
# allowed to edit the object
|
|
||||||
show_or_update = "update"
|
|
||||||
else:
|
|
||||||
# the activity for an object (i.e. the activity metadata) can be viewed
|
|
||||||
# if the user can see the object
|
|
||||||
show_or_update = "show"
|
|
||||||
action_on_which_to_base_auth = "{}_{}".format(
|
|
||||||
data_dict["object_type"], show_or_update
|
|
||||||
) # e.g. 'package_update'
|
|
||||||
return authz.is_authorized(
|
|
||||||
action_on_which_to_base_auth, context, {"id": data_dict["id"]}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@tk.auth_allow_anonymous_access
|
|
||||||
def user_activity_list(context: Context, data_dict: DataDict) -> AuthResult:
|
|
||||||
data_dict["object_type"] = "user"
|
|
||||||
# TODO: use authz.is_authorized in order to allow chained auth functions.
|
|
||||||
# TODO: fix the issue in other functions as well
|
|
||||||
return activity_list(context, data_dict)
|
|
||||||
|
|
||||||
|
|
||||||
@tk.auth_allow_anonymous_access
|
|
||||||
def package_activity_list(context: Context, data_dict: DataDict) -> AuthResult:
|
|
||||||
data_dict["object_type"] = "package"
|
|
||||||
return activity_list(context, data_dict)
|
|
||||||
|
|
||||||
|
|
||||||
@tk.auth_allow_anonymous_access
|
|
||||||
def group_activity_list(context: Context, data_dict: DataDict) -> AuthResult:
|
|
||||||
data_dict["object_type"] = "group"
|
|
||||||
return activity_list(context, data_dict)
|
|
||||||
|
|
||||||
|
|
||||||
@tk.auth_allow_anonymous_access
|
|
||||||
def organization_activity_list(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> AuthResult:
|
|
||||||
data_dict["object_type"] = "organization"
|
|
||||||
return activity_list(context, data_dict)
|
|
||||||
|
|
||||||
|
|
||||||
@tk.auth_allow_anonymous_access
|
|
||||||
def activity_show(context: Context, data_dict: DataDict) -> AuthResult:
|
|
||||||
"""
|
|
||||||
:param id: the id of the activity
|
|
||||||
:type id: string
|
|
||||||
:param include_data: include the data field, containing a full object dict
|
|
||||||
(otherwise the data field is only returned with the object's title)
|
|
||||||
:type include_data: boolean
|
|
||||||
"""
|
|
||||||
activity = _get_activity_object(context, data_dict)
|
|
||||||
# NB it would be better to have recorded an activity_type against the
|
|
||||||
# activity
|
|
||||||
if "package" in activity.activity_type:
|
|
||||||
object_type = "package"
|
|
||||||
else:
|
|
||||||
return {"success": False, "msg": "object_type not recognized"}
|
|
||||||
return activity_list(
|
|
||||||
context,
|
|
||||||
{
|
|
||||||
"id": activity.object_id,
|
|
||||||
"include_data": data_dict["include_data"],
|
|
||||||
"object_type": object_type,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@tk.auth_allow_anonymous_access
|
|
||||||
def activity_data_show(context: Context, data_dict: DataDict) -> AuthResult:
|
|
||||||
"""
|
|
||||||
:param id: the id of the activity
|
|
||||||
:type id: string
|
|
||||||
"""
|
|
||||||
data_dict["include_data"] = True
|
|
||||||
return activity_show(context, data_dict)
|
|
||||||
|
|
||||||
|
|
||||||
@tk.auth_allow_anonymous_access
|
|
||||||
def activity_diff(context: Context, data_dict: DataDict) -> AuthResult:
|
|
||||||
"""
|
|
||||||
:param id: the id of the activity
|
|
||||||
:type id: string
|
|
||||||
"""
|
|
||||||
data_dict["include_data"] = True
|
|
||||||
return activity_show(context, data_dict)
|
|
||||||
|
|
||||||
|
|
||||||
def dashboard_mark_activities_old(
|
|
||||||
context: Context, data_dict: DataDict
|
|
||||||
) -> AuthResult:
|
|
||||||
return authz.is_authorized("dashboard_activity_list", context, data_dict)
|
|
|
@ -1,102 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from ckan.logic.schema import validator_args, default_pagination_schema
|
|
||||||
from ckan.types import Schema, Validator, ValidatorFactory
|
|
||||||
|
|
||||||
|
|
||||||
@validator_args
|
|
||||||
def default_create_activity_schema(
|
|
||||||
ignore: Validator,
|
|
||||||
not_missing: Validator,
|
|
||||||
not_empty: Validator,
|
|
||||||
unicode_safe: Validator,
|
|
||||||
convert_user_name_or_id_to_id: Validator,
|
|
||||||
object_id_validator: Validator,
|
|
||||||
activity_type_exists: Validator,
|
|
||||||
ignore_empty: Validator,
|
|
||||||
ignore_missing: Validator,
|
|
||||||
):
|
|
||||||
return cast(
|
|
||||||
Schema,
|
|
||||||
{
|
|
||||||
"id": [ignore],
|
|
||||||
"timestamp": [ignore],
|
|
||||||
"user_id": [
|
|
||||||
not_missing,
|
|
||||||
not_empty,
|
|
||||||
unicode_safe,
|
|
||||||
convert_user_name_or_id_to_id,
|
|
||||||
],
|
|
||||||
"object_id": [
|
|
||||||
not_missing,
|
|
||||||
not_empty,
|
|
||||||
unicode_safe,
|
|
||||||
object_id_validator,
|
|
||||||
],
|
|
||||||
"activity_type": [
|
|
||||||
not_missing,
|
|
||||||
not_empty,
|
|
||||||
unicode_safe,
|
|
||||||
activity_type_exists,
|
|
||||||
],
|
|
||||||
"data": [ignore_empty, ignore_missing],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@validator_args
|
|
||||||
def default_dashboard_activity_list_schema(
|
|
||||||
configured_default: ValidatorFactory,
|
|
||||||
natural_number_validator: Validator,
|
|
||||||
limit_to_configured_maximum: ValidatorFactory,
|
|
||||||
ignore_missing: Validator,
|
|
||||||
datetime_from_timestamp_validator: Validator,
|
|
||||||
|
|
||||||
):
|
|
||||||
schema = default_pagination_schema()
|
|
||||||
schema["limit"] = [
|
|
||||||
configured_default("ckan.activity_list_limit", 31),
|
|
||||||
natural_number_validator,
|
|
||||||
limit_to_configured_maximum("ckan.activity_list_limit_max", 100),
|
|
||||||
]
|
|
||||||
schema["before"] = [ignore_missing, datetime_from_timestamp_validator]
|
|
||||||
schema["after"] = [ignore_missing, datetime_from_timestamp_validator]
|
|
||||||
return schema
|
|
||||||
|
|
||||||
|
|
||||||
@validator_args
|
|
||||||
def default_activity_list_schema(
|
|
||||||
not_missing: Validator,
|
|
||||||
unicode_safe: Validator,
|
|
||||||
configured_default: ValidatorFactory,
|
|
||||||
natural_number_validator: Validator,
|
|
||||||
limit_to_configured_maximum: ValidatorFactory,
|
|
||||||
ignore_missing: Validator,
|
|
||||||
boolean_validator: Validator,
|
|
||||||
ignore_not_sysadmin: Validator,
|
|
||||||
list_of_strings: Validator,
|
|
||||||
datetime_from_timestamp_validator: Validator,
|
|
||||||
):
|
|
||||||
|
|
||||||
schema = default_pagination_schema()
|
|
||||||
schema["id"] = [not_missing, unicode_safe]
|
|
||||||
schema["limit"] = [
|
|
||||||
configured_default("ckan.activity_list_limit", 31),
|
|
||||||
natural_number_validator,
|
|
||||||
limit_to_configured_maximum("ckan.activity_list_limit_max", 100),
|
|
||||||
]
|
|
||||||
schema["include_hidden_activity"] = [
|
|
||||||
ignore_missing,
|
|
||||||
ignore_not_sysadmin,
|
|
||||||
boolean_validator,
|
|
||||||
]
|
|
||||||
schema["activity_types"] = [ignore_missing, list_of_strings]
|
|
||||||
schema["exclude_activity_types"] = [ignore_missing, list_of_strings]
|
|
||||||
schema["before"] = [ignore_missing, datetime_from_timestamp_validator]
|
|
||||||
schema["after"] = [ignore_missing, datetime_from_timestamp_validator]
|
|
||||||
|
|
||||||
return schema
|
|
|
@ -1,97 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Any, cast
|
|
||||||
|
|
||||||
import ckan.plugins.toolkit as tk
|
|
||||||
|
|
||||||
from ckan.types import (
|
|
||||||
FlattenDataDict,
|
|
||||||
FlattenKey,
|
|
||||||
Context,
|
|
||||||
FlattenErrorDict,
|
|
||||||
ContextValidator,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def activity_type_exists(activity_type: Any) -> Any:
|
|
||||||
"""Raises Invalid if there is no registered activity renderer for the
|
|
||||||
given activity_type. Otherwise returns the given activity_type.
|
|
||||||
|
|
||||||
This just uses object_id_validators as a lookup.
|
|
||||||
very safe.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if activity_type in object_id_validators:
|
|
||||||
return activity_type
|
|
||||||
else:
|
|
||||||
raise tk.Invalid("%s: %s" % (tk._("Not found"), tk._("Activity type")))
|
|
||||||
|
|
||||||
|
|
||||||
VALIDATORS_PACKAGE_ACTIVITY_TYPES = {
|
|
||||||
"new package": "package_id_exists",
|
|
||||||
"changed package": "package_id_exists",
|
|
||||||
"deleted package": "package_id_exists",
|
|
||||||
"follow dataset": "package_id_exists",
|
|
||||||
}
|
|
||||||
|
|
||||||
VALIDATORS_USER_ACTIVITY_TYPES = {
|
|
||||||
"new user": "user_id_exists",
|
|
||||||
"changed user": "user_id_exists",
|
|
||||||
"follow user": "user_id_exists",
|
|
||||||
}
|
|
||||||
|
|
||||||
VALIDATORS_GROUP_ACTIVITY_TYPES = {
|
|
||||||
"new group": "group_id_exists",
|
|
||||||
"changed group": "group_id_exists",
|
|
||||||
"deleted group": "group_id_exists",
|
|
||||||
"follow group": "group_id_exists",
|
|
||||||
}
|
|
||||||
|
|
||||||
VALIDATORS_ORGANIZATION_ACTIVITY_TYPES = {
|
|
||||||
"new organization": "group_id_exists",
|
|
||||||
"changed organization": "group_id_exists",
|
|
||||||
"deleted organization": "group_id_exists",
|
|
||||||
"follow organization": "group_id_exists",
|
|
||||||
}
|
|
||||||
|
|
||||||
# A dictionary mapping activity_type values from activity dicts to functions
|
|
||||||
# for validating the object_id values from those same activity dicts.
|
|
||||||
object_id_validators = {
|
|
||||||
**VALIDATORS_PACKAGE_ACTIVITY_TYPES,
|
|
||||||
**VALIDATORS_USER_ACTIVITY_TYPES,
|
|
||||||
**VALIDATORS_GROUP_ACTIVITY_TYPES,
|
|
||||||
**VALIDATORS_ORGANIZATION_ACTIVITY_TYPES
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def object_id_validator(
|
|
||||||
key: FlattenKey,
|
|
||||||
activity_dict: FlattenDataDict,
|
|
||||||
errors: FlattenErrorDict,
|
|
||||||
context: Context,
|
|
||||||
) -> Any:
|
|
||||||
"""Validate the 'object_id' value of an activity_dict.
|
|
||||||
|
|
||||||
Uses the object_id_validators dict (above) to find and call an 'object_id'
|
|
||||||
validator function for the given activity_dict's 'activity_type' value.
|
|
||||||
|
|
||||||
Raises Invalid if the model given in context contains no object of the
|
|
||||||
correct type (according to the 'activity_type' value of the activity_dict)
|
|
||||||
with the given ID.
|
|
||||||
|
|
||||||
Raises Invalid if there is no object_id_validator for the activity_dict's
|
|
||||||
'activity_type' value.
|
|
||||||
|
|
||||||
"""
|
|
||||||
activity_type = activity_dict[("activity_type",)]
|
|
||||||
if activity_type in object_id_validators:
|
|
||||||
object_id = activity_dict[("object_id",)]
|
|
||||||
name = object_id_validators[activity_type]
|
|
||||||
validator = cast(ContextValidator, tk.get_validator(name))
|
|
||||||
return validator(object_id, context)
|
|
||||||
else:
|
|
||||||
raise tk.Invalid(
|
|
||||||
'There is no object_id validator for activity type "%s"'
|
|
||||||
% activity_type
|
|
||||||
)
|
|
|
@ -1,28 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from ckan.types import Context
|
|
||||||
|
|
||||||
from .activity import Activity
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Activity"]
|
|
||||||
|
|
||||||
|
|
||||||
def activity_dict_save(
|
|
||||||
activity_dict: dict[str, Any], context: Context
|
|
||||||
) -> "Activity":
|
|
||||||
|
|
||||||
session = context["session"]
|
|
||||||
user_id = activity_dict["user_id"]
|
|
||||||
object_id = activity_dict["object_id"]
|
|
||||||
activity_type = activity_dict["activity_type"]
|
|
||||||
if "data" in activity_dict:
|
|
||||||
data = activity_dict["data"]
|
|
||||||
else:
|
|
||||||
data = None
|
|
||||||
activity_obj = Activity(user_id, object_id, activity_type, data)
|
|
||||||
session.add(activity_obj)
|
|
||||||
return activity_obj
|
|
|
@ -1,791 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
from typing import Any, Optional, Type, TypeVar, cast
|
|
||||||
from typing_extensions import TypeAlias
|
|
||||||
|
|
||||||
from sqlalchemy.orm import relationship, backref
|
|
||||||
from sqlalchemy import (
|
|
||||||
types,
|
|
||||||
Column,
|
|
||||||
ForeignKey,
|
|
||||||
or_,
|
|
||||||
and_,
|
|
||||||
union_all,
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ckan.common import config
|
|
||||||
import ckan.model as model
|
|
||||||
import ckan.model.meta as meta
|
|
||||||
import ckan.model.domain_object as domain_object
|
|
||||||
import ckan.model.types as _types
|
|
||||||
from ckan.model.base import BaseModel
|
|
||||||
from ckan.lib.dictization import table_dictize
|
|
||||||
|
|
||||||
from ckan.types import Context, Query # noqa
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Activity", "ActivityDetail"]
|
|
||||||
|
|
||||||
TActivityDetail = TypeVar("TActivityDetail", bound="ActivityDetail")
|
|
||||||
QActivity: TypeAlias = "Query[Activity]"
|
|
||||||
|
|
||||||
|
|
||||||
class Activity(domain_object.DomainObject, BaseModel): # type: ignore
|
|
||||||
__tablename__ = "activity"
|
|
||||||
# the line below handles cases when activity table was already loaded into
|
|
||||||
# metadata state(via stats extension). Can be removed if stats stop using
|
|
||||||
# Table object.
|
|
||||||
__table_args__ = {"extend_existing": True}
|
|
||||||
|
|
||||||
id = Column(
|
|
||||||
"id", types.UnicodeText, primary_key=True, default=_types.make_uuid
|
|
||||||
)
|
|
||||||
timestamp = Column("timestamp", types.DateTime)
|
|
||||||
user_id = Column("user_id", types.UnicodeText)
|
|
||||||
object_id = Column("object_id", types.UnicodeText)
|
|
||||||
# legacy revision_id values are used by migrate_package_activity.py
|
|
||||||
revision_id = Column("revision_id", types.UnicodeText)
|
|
||||||
activity_type = Column("activity_type", types.UnicodeText)
|
|
||||||
data = Column("data", _types.JsonDictType)
|
|
||||||
|
|
||||||
activity_detail: "ActivityDetail"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
user_id: str,
|
|
||||||
object_id: str,
|
|
||||||
activity_type: str,
|
|
||||||
data: Optional[dict[str, Any]] = None,
|
|
||||||
) -> None:
|
|
||||||
self.id = _types.make_uuid()
|
|
||||||
self.timestamp = datetime.datetime.utcnow()
|
|
||||||
self.user_id = user_id
|
|
||||||
self.object_id = object_id
|
|
||||||
self.activity_type = activity_type
|
|
||||||
if data is None:
|
|
||||||
self.data = {}
|
|
||||||
else:
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls, id: str) -> Optional["Activity"]:
|
|
||||||
"""Returns an Activity object referenced by its id."""
|
|
||||||
if not id:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return meta.Session.query(cls).get(id)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def activity_stream_item(
|
|
||||||
cls, pkg: model.Package, activity_type: str, user_id: str
|
|
||||||
) -> Optional["Activity"]:
|
|
||||||
import ckan.model
|
|
||||||
import ckan.logic
|
|
||||||
|
|
||||||
assert activity_type in ("new", "changed"), str(activity_type)
|
|
||||||
|
|
||||||
# Handle 'deleted' objects.
|
|
||||||
# When the user marks a package as deleted this comes through here as
|
|
||||||
# a 'changed' package activity. We detect this and change it to a
|
|
||||||
# 'deleted' activity.
|
|
||||||
if activity_type == "changed" and pkg.state == "deleted":
|
|
||||||
if (
|
|
||||||
meta.Session.query(cls)
|
|
||||||
.filter_by(object_id=pkg.id, activity_type="deleted")
|
|
||||||
.all()
|
|
||||||
):
|
|
||||||
# A 'deleted' activity for this object has already been emitted
|
|
||||||
# FIXME: What if the object was deleted and then activated
|
|
||||||
# again?
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
# Emit a 'deleted' activity for this object.
|
|
||||||
activity_type = "deleted"
|
|
||||||
|
|
||||||
try:
|
|
||||||
# We save the entire rendered package dict so we can support
|
|
||||||
# viewing the past packages from the activity feed.
|
|
||||||
dictized_package = ckan.logic.get_action("package_show")(
|
|
||||||
cast(
|
|
||||||
Context,
|
|
||||||
{
|
|
||||||
"model": ckan.model,
|
|
||||||
"session": ckan.model.Session,
|
|
||||||
# avoid ckanext-multilingual translating it
|
|
||||||
"for_view": False,
|
|
||||||
"ignore_auth": True,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
{"id": pkg.id, "include_tracking": False},
|
|
||||||
)
|
|
||||||
except ckan.logic.NotFound:
|
|
||||||
# This happens if this package is being purged and therefore has no
|
|
||||||
# current revision.
|
|
||||||
# TODO: Purge all related activity stream items when a model object
|
|
||||||
# is purged.
|
|
||||||
return None
|
|
||||||
|
|
||||||
actor = meta.Session.query(ckan.model.User).get(user_id)
|
|
||||||
|
|
||||||
return cls(
|
|
||||||
user_id,
|
|
||||||
pkg.id,
|
|
||||||
"%s package" % activity_type,
|
|
||||||
{
|
|
||||||
"package": dictized_package,
|
|
||||||
# We keep the acting user name around so that actions can be
|
|
||||||
# properly displayed even if the user is deleted in the future.
|
|
||||||
"actor": actor.name if actor else None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def activity_dictize(activity: Activity, context: Context) -> dict[str, Any]:
|
|
||||||
return table_dictize(activity, context)
|
|
||||||
|
|
||||||
|
|
||||||
def activity_list_dictize(
|
|
||||||
activity_list: list[Activity], context: Context
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
return [activity_dictize(activity, context) for activity in activity_list]
|
|
||||||
|
|
||||||
|
|
||||||
# deprecated
|
|
||||||
class ActivityDetail(domain_object.DomainObject):
|
|
||||||
__tablename__ = "activity_detail"
|
|
||||||
id = Column(
|
|
||||||
"id", types.UnicodeText, primary_key=True, default=_types.make_uuid
|
|
||||||
)
|
|
||||||
activity_id = Column(
|
|
||||||
"activity_id", types.UnicodeText, ForeignKey("activity.id")
|
|
||||||
)
|
|
||||||
object_id = Column("object_id", types.UnicodeText)
|
|
||||||
object_type = Column("object_type", types.UnicodeText)
|
|
||||||
activity_type = Column("activity_type", types.UnicodeText)
|
|
||||||
data = Column("data", _types.JsonDictType)
|
|
||||||
|
|
||||||
activity = relationship(
|
|
||||||
Activity,
|
|
||||||
backref=backref("activity_detail", cascade="all, delete-orphan"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
activity_id: str,
|
|
||||||
object_id: str,
|
|
||||||
object_type: str,
|
|
||||||
activity_type: str,
|
|
||||||
data: Optional[dict[str, Any]] = None,
|
|
||||||
) -> None:
|
|
||||||
self.activity_id = activity_id
|
|
||||||
self.object_id = object_id
|
|
||||||
self.object_type = object_type
|
|
||||||
self.activity_type = activity_type
|
|
||||||
if data is None:
|
|
||||||
self.data = {}
|
|
||||||
else:
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def by_activity_id(
|
|
||||||
cls: Type[TActivityDetail], activity_id: str
|
|
||||||
) -> list["TActivityDetail"]:
|
|
||||||
return (
|
|
||||||
model.Session.query(cls).filter_by(activity_id=activity_id).all()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _activities_limit(
|
|
||||||
q: QActivity,
|
|
||||||
limit: int,
|
|
||||||
offset: Optional[int] = None,
|
|
||||||
revese_order: Optional[bool] = False,
|
|
||||||
) -> QActivity:
|
|
||||||
"""
|
|
||||||
Return an SQLAlchemy query for all activities at an offset with a limit.
|
|
||||||
|
|
||||||
revese_order:
|
|
||||||
if we want the last activities before a date, we must reverse the
|
|
||||||
order before limiting.
|
|
||||||
"""
|
|
||||||
if revese_order:
|
|
||||||
q = q.order_by(Activity.timestamp)
|
|
||||||
else:
|
|
||||||
# type_ignore_reason: incomplete SQLAlchemy types
|
|
||||||
q = q.order_by(Activity.timestamp.desc()) # type: ignore
|
|
||||||
|
|
||||||
if offset:
|
|
||||||
q = q.offset(offset)
|
|
||||||
if limit:
|
|
||||||
q = q.limit(limit)
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def _activities_union_all(*qlist: QActivity) -> QActivity:
|
|
||||||
"""
|
|
||||||
Return union of two or more activity queries sorted by timestamp,
|
|
||||||
and remove duplicates
|
|
||||||
"""
|
|
||||||
q: QActivity = (
|
|
||||||
model.Session.query(Activity)
|
|
||||||
.select_entity_from(union_all(*[q.subquery().select() for q in qlist]))
|
|
||||||
.distinct(Activity.timestamp)
|
|
||||||
)
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def _activities_from_user_query(user_id: str) -> QActivity:
|
|
||||||
"""Return an SQLAlchemy query for all activities from user_id."""
|
|
||||||
q = model.Session.query(Activity)
|
|
||||||
q = q.filter(Activity.user_id == user_id)
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def _activities_about_user_query(user_id: str) -> QActivity:
|
|
||||||
"""Return an SQLAlchemy query for all activities about user_id."""
|
|
||||||
q = model.Session.query(Activity)
|
|
||||||
q = q.filter(Activity.object_id == user_id)
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def _user_activity_query(user_id: str, limit: int) -> QActivity:
|
|
||||||
"""Return an SQLAlchemy query for all activities from or about user_id."""
|
|
||||||
q1 = _activities_limit(_activities_from_user_query(user_id), limit)
|
|
||||||
q2 = _activities_limit(_activities_about_user_query(user_id), limit)
|
|
||||||
return _activities_union_all(q1, q2)
|
|
||||||
|
|
||||||
|
|
||||||
def user_activity_list(
|
|
||||||
user_id: str,
|
|
||||||
limit: int,
|
|
||||||
offset: int,
|
|
||||||
after: Optional[datetime.datetime] = None,
|
|
||||||
before: Optional[datetime.datetime] = None,
|
|
||||||
) -> list[Activity]:
|
|
||||||
"""Return user_id's public activity stream.
|
|
||||||
|
|
||||||
Return a list of all activities from or about the given user, i.e. where
|
|
||||||
the given user is the subject or object of the activity, e.g.:
|
|
||||||
|
|
||||||
"{USER} created the dataset {DATASET}"
|
|
||||||
"{OTHER_USER} started following {USER}"
|
|
||||||
etc.
|
|
||||||
|
|
||||||
"""
|
|
||||||
q1 = _activities_from_user_query(user_id)
|
|
||||||
q2 = _activities_about_user_query(user_id)
|
|
||||||
|
|
||||||
q = _activities_union_all(q1, q2)
|
|
||||||
|
|
||||||
q = _filter_activitites_from_users(q)
|
|
||||||
|
|
||||||
if after:
|
|
||||||
q = q.filter(Activity.timestamp > after)
|
|
||||||
if before:
|
|
||||||
q = q.filter(Activity.timestamp < before)
|
|
||||||
|
|
||||||
# revert sort queries for "only before" queries
|
|
||||||
revese_order = after and not before
|
|
||||||
if revese_order:
|
|
||||||
q = q.order_by(Activity.timestamp)
|
|
||||||
else:
|
|
||||||
# type_ignore_reason: incomplete SQLAlchemy types
|
|
||||||
q = q.order_by(Activity.timestamp.desc()) # type: ignore
|
|
||||||
|
|
||||||
if offset:
|
|
||||||
q = q.offset(offset)
|
|
||||||
if limit:
|
|
||||||
q = q.limit(limit)
|
|
||||||
|
|
||||||
results = q.all()
|
|
||||||
|
|
||||||
# revert result if required
|
|
||||||
if revese_order:
|
|
||||||
results.reverse()
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def _package_activity_query(package_id: str) -> QActivity:
|
|
||||||
"""Return an SQLAlchemy query for all activities about package_id."""
|
|
||||||
q = model.Session.query(Activity).filter_by(object_id=package_id)
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def package_activity_list(
|
|
||||||
package_id: str,
|
|
||||||
limit: int,
|
|
||||||
offset: Optional[int] = None,
|
|
||||||
after: Optional[datetime.datetime] = None,
|
|
||||||
before: Optional[datetime.datetime] = None,
|
|
||||||
include_hidden_activity: bool = False,
|
|
||||||
activity_types: Optional[list[str]] = None,
|
|
||||||
exclude_activity_types: Optional[list[str]] = None,
|
|
||||||
) -> list[Activity]:
|
|
||||||
"""Return the given dataset (package)'s public activity stream.
|
|
||||||
|
|
||||||
Returns all activities about the given dataset, i.e. where the given
|
|
||||||
dataset is the object of the activity, e.g.:
|
|
||||||
|
|
||||||
"{USER} created the dataset {DATASET}"
|
|
||||||
"{USER} updated the dataset {DATASET}"
|
|
||||||
etc.
|
|
||||||
|
|
||||||
"""
|
|
||||||
q = _package_activity_query(package_id)
|
|
||||||
|
|
||||||
if not include_hidden_activity:
|
|
||||||
q = _filter_activitites_from_users(q)
|
|
||||||
|
|
||||||
if activity_types:
|
|
||||||
q = _filter_activitites_from_type(
|
|
||||||
q, include=True, types=activity_types
|
|
||||||
)
|
|
||||||
elif exclude_activity_types:
|
|
||||||
q = _filter_activitites_from_type(
|
|
||||||
q, include=False, types=exclude_activity_types
|
|
||||||
)
|
|
||||||
|
|
||||||
if after:
|
|
||||||
q = q.filter(Activity.timestamp > after)
|
|
||||||
if before:
|
|
||||||
q = q.filter(Activity.timestamp < before)
|
|
||||||
|
|
||||||
# revert sort queries for "only before" queries
|
|
||||||
revese_order = after and not before
|
|
||||||
if revese_order:
|
|
||||||
q = q.order_by(Activity.timestamp)
|
|
||||||
else:
|
|
||||||
# type_ignore_reason: incomplete SQLAlchemy types
|
|
||||||
q = q.order_by(Activity.timestamp.desc()) # type: ignore
|
|
||||||
|
|
||||||
if offset:
|
|
||||||
q = q.offset(offset)
|
|
||||||
if limit:
|
|
||||||
q = q.limit(limit)
|
|
||||||
|
|
||||||
results = q.all()
|
|
||||||
|
|
||||||
# revert result if required
|
|
||||||
if revese_order:
|
|
||||||
results.reverse()
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def _group_activity_query(group_id: str) -> QActivity:
|
|
||||||
"""Return an SQLAlchemy query for all activities about group_id.
|
|
||||||
|
|
||||||
Returns a query for all activities whose object is either the group itself
|
|
||||||
or one of the group's datasets.
|
|
||||||
|
|
||||||
"""
|
|
||||||
group = model.Group.get(group_id)
|
|
||||||
if not group:
|
|
||||||
# Return a query with no results.
|
|
||||||
return model.Session.query(Activity).filter(text("0=1"))
|
|
||||||
|
|
||||||
q: QActivity = (
|
|
||||||
model.Session.query(Activity)
|
|
||||||
.outerjoin(model.Member, Activity.object_id == model.Member.table_id)
|
|
||||||
.outerjoin(
|
|
||||||
model.Package,
|
|
||||||
and_(
|
|
||||||
model.Package.id == model.Member.table_id,
|
|
||||||
model.Package.private == False, # noqa
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.filter(
|
|
||||||
# We only care about activity either on the group itself or on
|
|
||||||
# packages within that group. FIXME: This means that activity that
|
|
||||||
# occured while a package belonged to a group but was then removed
|
|
||||||
# will not show up. This may not be desired but is consistent with
|
|
||||||
# legacy behaviour.
|
|
||||||
or_(
|
|
||||||
# active dataset in the group
|
|
||||||
and_(
|
|
||||||
model.Member.group_id == group_id,
|
|
||||||
model.Member.state == "active",
|
|
||||||
model.Package.state == "active",
|
|
||||||
),
|
|
||||||
# deleted dataset in the group
|
|
||||||
and_(
|
|
||||||
model.Member.group_id == group_id,
|
|
||||||
model.Member.state == "deleted",
|
|
||||||
model.Package.state == "deleted",
|
|
||||||
),
|
|
||||||
# (we want to avoid showing changes to an active dataset that
|
|
||||||
# was once in this group)
|
|
||||||
# activity the the group itself
|
|
||||||
Activity.object_id == group_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def _organization_activity_query(org_id: str) -> QActivity:
|
|
||||||
"""Return an SQLAlchemy query for all activities about org_id.
|
|
||||||
|
|
||||||
Returns a query for all activities whose object is either the org itself
|
|
||||||
or one of the org's datasets.
|
|
||||||
|
|
||||||
"""
|
|
||||||
org = model.Group.get(org_id)
|
|
||||||
if not org or not org.is_organization:
|
|
||||||
# Return a query with no results.
|
|
||||||
return model.Session.query(Activity).filter(text("0=1"))
|
|
||||||
|
|
||||||
q: QActivity = (
|
|
||||||
model.Session.query(Activity)
|
|
||||||
.outerjoin(
|
|
||||||
model.Package,
|
|
||||||
and_(
|
|
||||||
model.Package.id == Activity.object_id,
|
|
||||||
model.Package.private == False, # noqa
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.filter(
|
|
||||||
# We only care about activity either on the the org itself or on
|
|
||||||
# packages within that org.
|
|
||||||
# FIXME: This means that activity that occured while a package
|
|
||||||
# belonged to a org but was then removed will not show up. This may
|
|
||||||
# not be desired but is consistent with legacy behaviour.
|
|
||||||
or_(
|
|
||||||
model.Package.owner_org == org_id, Activity.object_id == org_id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def group_activity_list(
|
|
||||||
group_id: str,
|
|
||||||
limit: int,
|
|
||||||
offset: int,
|
|
||||||
after: Optional[datetime.datetime] = None,
|
|
||||||
before: Optional[datetime.datetime] = None,
|
|
||||||
include_hidden_activity: bool = False,
|
|
||||||
activity_types: Optional[list[str]] = None
|
|
||||||
) -> list[Activity]:
|
|
||||||
|
|
||||||
"""Return the given group's public activity stream.
|
|
||||||
|
|
||||||
Returns activities where the given group or one of its datasets is the
|
|
||||||
object of the activity, e.g.:
|
|
||||||
|
|
||||||
"{USER} updated the group {GROUP}"
|
|
||||||
"{USER} updated the dataset {DATASET}"
|
|
||||||
etc.
|
|
||||||
|
|
||||||
"""
|
|
||||||
q = _group_activity_query(group_id)
|
|
||||||
|
|
||||||
if not include_hidden_activity:
|
|
||||||
q = _filter_activitites_from_users(q)
|
|
||||||
|
|
||||||
if activity_types:
|
|
||||||
q = _filter_activitites_from_type(
|
|
||||||
q, include=True, types=activity_types
|
|
||||||
)
|
|
||||||
|
|
||||||
if after:
|
|
||||||
q = q.filter(Activity.timestamp > after)
|
|
||||||
if before:
|
|
||||||
q = q.filter(Activity.timestamp < before)
|
|
||||||
|
|
||||||
# revert sort queries for "only before" queries
|
|
||||||
revese_order = after and not before
|
|
||||||
if revese_order:
|
|
||||||
q = q.order_by(Activity.timestamp)
|
|
||||||
else:
|
|
||||||
# type_ignore_reason: incomplete SQLAlchemy types
|
|
||||||
q = q.order_by(Activity.timestamp.desc()) # type: ignore
|
|
||||||
|
|
||||||
if offset:
|
|
||||||
q = q.offset(offset)
|
|
||||||
if limit:
|
|
||||||
q = q.limit(limit)
|
|
||||||
|
|
||||||
results = q.all()
|
|
||||||
|
|
||||||
# revert result if required
|
|
||||||
if revese_order:
|
|
||||||
results.reverse()
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def organization_activity_list(
|
|
||||||
group_id: str,
|
|
||||||
limit: int,
|
|
||||||
offset: int,
|
|
||||||
after: Optional[datetime.datetime] = None,
|
|
||||||
before: Optional[datetime.datetime] = None,
|
|
||||||
include_hidden_activity: bool = False,
|
|
||||||
activity_types: Optional[list[str]] = None
|
|
||||||
) -> list[Activity]:
|
|
||||||
"""Return the given org's public activity stream.
|
|
||||||
|
|
||||||
Returns activities where the given org or one of its datasets is the
|
|
||||||
object of the activity, e.g.:
|
|
||||||
|
|
||||||
"{USER} updated the organization {ORG}"
|
|
||||||
"{USER} updated the dataset {DATASET}"
|
|
||||||
etc.
|
|
||||||
|
|
||||||
"""
|
|
||||||
q = _organization_activity_query(group_id)
|
|
||||||
|
|
||||||
if not include_hidden_activity:
|
|
||||||
q = _filter_activitites_from_users(q)
|
|
||||||
|
|
||||||
if activity_types:
|
|
||||||
q = _filter_activitites_from_type(
|
|
||||||
q, include=True, types=activity_types
|
|
||||||
)
|
|
||||||
|
|
||||||
if after:
|
|
||||||
q = q.filter(Activity.timestamp > after)
|
|
||||||
if before:
|
|
||||||
q = q.filter(Activity.timestamp < before)
|
|
||||||
|
|
||||||
# revert sort queries for "only before" queries
|
|
||||||
revese_order = after and not before
|
|
||||||
if revese_order:
|
|
||||||
q = q.order_by(Activity.timestamp)
|
|
||||||
else:
|
|
||||||
# type_ignore_reason: incomplete SQLAlchemy types
|
|
||||||
q = q.order_by(Activity.timestamp.desc()) # type: ignore
|
|
||||||
|
|
||||||
if offset:
|
|
||||||
q = q.offset(offset)
|
|
||||||
if limit:
|
|
||||||
q = q.limit(limit)
|
|
||||||
|
|
||||||
results = q.all()
|
|
||||||
|
|
||||||
# revert result if required
|
|
||||||
if revese_order:
|
|
||||||
results.reverse()
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def _activities_from_users_followed_by_user_query(
|
|
||||||
user_id: str, limit: int
|
|
||||||
) -> QActivity:
|
|
||||||
"""Return a query for all activities from users that user_id follows."""
|
|
||||||
|
|
||||||
# Get a list of the users that the given user is following.
|
|
||||||
follower_objects = model.UserFollowingUser.followee_list(user_id)
|
|
||||||
if not follower_objects:
|
|
||||||
# Return a query with no results.
|
|
||||||
return model.Session.query(Activity).filter(text("0=1"))
|
|
||||||
|
|
||||||
return _activities_union_all(
|
|
||||||
*[
|
|
||||||
_user_activity_query(follower.object_id, limit)
|
|
||||||
for follower in follower_objects
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _activities_from_datasets_followed_by_user_query(
|
|
||||||
user_id: str, limit: int
|
|
||||||
) -> QActivity:
|
|
||||||
"""Return a query for all activities from datasets that user_id follows."""
|
|
||||||
# Get a list of the datasets that the user is following.
|
|
||||||
follower_objects = model.UserFollowingDataset.followee_list(user_id)
|
|
||||||
if not follower_objects:
|
|
||||||
# Return a query with no results.
|
|
||||||
return model.Session.query(Activity).filter(text("0=1"))
|
|
||||||
|
|
||||||
return _activities_union_all(
|
|
||||||
*[
|
|
||||||
_activities_limit(
|
|
||||||
_package_activity_query(follower.object_id), limit
|
|
||||||
)
|
|
||||||
for follower in follower_objects
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _activities_from_groups_followed_by_user_query(
|
|
||||||
user_id: str, limit: int
|
|
||||||
) -> QActivity:
|
|
||||||
"""Return a query for all activities about groups the given user follows.
|
|
||||||
|
|
||||||
Return a query for all activities about the groups the given user follows,
|
|
||||||
or about any of the group's datasets. This is the union of
|
|
||||||
_group_activity_query(group_id) for each of the groups the user follows.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Get a list of the group's that the user is following.
|
|
||||||
follower_objects = model.UserFollowingGroup.followee_list(user_id)
|
|
||||||
if not follower_objects:
|
|
||||||
# Return a query with no results.
|
|
||||||
return model.Session.query(Activity).filter(text("0=1"))
|
|
||||||
|
|
||||||
return _activities_union_all(
|
|
||||||
*[
|
|
||||||
_activities_limit(_group_activity_query(follower.object_id), limit)
|
|
||||||
for follower in follower_objects
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _activities_from_everything_followed_by_user_query(
|
|
||||||
user_id: str, limit: int = 0
|
|
||||||
) -> QActivity:
|
|
||||||
"""Return a query for all activities from everything user_id follows."""
|
|
||||||
q1 = _activities_from_users_followed_by_user_query(user_id, limit)
|
|
||||||
q2 = _activities_from_datasets_followed_by_user_query(user_id, limit)
|
|
||||||
q3 = _activities_from_groups_followed_by_user_query(user_id, limit)
|
|
||||||
return _activities_union_all(q1, q2, q3)
|
|
||||||
|
|
||||||
|
|
||||||
def activities_from_everything_followed_by_user(
|
|
||||||
user_id: str, limit: int, offset: int
|
|
||||||
) -> list[Activity]:
|
|
||||||
"""Return activities from everything that the given user is following.
|
|
||||||
|
|
||||||
Returns all activities where the object of the activity is anything
|
|
||||||
(user, dataset, group...) that the given user is following.
|
|
||||||
|
|
||||||
"""
|
|
||||||
q = _activities_from_everything_followed_by_user_query(
|
|
||||||
user_id, limit + offset
|
|
||||||
)
|
|
||||||
return _activities_limit(q, limit, offset).all()
|
|
||||||
|
|
||||||
|
|
||||||
def _dashboard_activity_query(user_id: str, limit: int = 0) -> QActivity:
|
|
||||||
"""Return an SQLAlchemy query for user_id's dashboard activity stream."""
|
|
||||||
q1 = _user_activity_query(user_id, limit)
|
|
||||||
q2 = _activities_from_everything_followed_by_user_query(user_id, limit)
|
|
||||||
return _activities_union_all(q1, q2)
|
|
||||||
|
|
||||||
|
|
||||||
def dashboard_activity_list(
|
|
||||||
user_id: str,
|
|
||||||
limit: int,
|
|
||||||
offset: int,
|
|
||||||
before: Optional[datetime.datetime] = None,
|
|
||||||
after: Optional[datetime.datetime] = None,
|
|
||||||
) -> list[Activity]:
|
|
||||||
"""Return the given user's dashboard activity stream.
|
|
||||||
|
|
||||||
Returns activities from the user's public activity stream, plus
|
|
||||||
activities from everything that the user is following.
|
|
||||||
|
|
||||||
This is the union of user_activity_list(user_id) and
|
|
||||||
activities_from_everything_followed_by_user(user_id).
|
|
||||||
|
|
||||||
"""
|
|
||||||
q = _dashboard_activity_query(user_id)
|
|
||||||
|
|
||||||
q = _filter_activitites_from_users(q)
|
|
||||||
|
|
||||||
if after:
|
|
||||||
q = q.filter(Activity.timestamp > after)
|
|
||||||
if before:
|
|
||||||
q = q.filter(Activity.timestamp < before)
|
|
||||||
|
|
||||||
# revert sort queries for "only before" queries
|
|
||||||
revese_order = after and not before
|
|
||||||
if revese_order:
|
|
||||||
q = q.order_by(Activity.timestamp)
|
|
||||||
else:
|
|
||||||
# type_ignore_reason: incomplete SQLAlchemy types
|
|
||||||
q = q.order_by(Activity.timestamp.desc()) # type: ignore
|
|
||||||
|
|
||||||
if offset:
|
|
||||||
q = q.offset(offset)
|
|
||||||
if limit:
|
|
||||||
q = q.limit(limit)
|
|
||||||
|
|
||||||
results = q.all()
|
|
||||||
|
|
||||||
# revert result if required
|
|
||||||
if revese_order:
|
|
||||||
results.reverse()
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def _changed_packages_activity_query() -> QActivity:
|
|
||||||
"""Return an SQLAlchemy query for all changed package activities.
|
|
||||||
|
|
||||||
Return a query for all activities with activity_type '*package', e.g.
|
|
||||||
'new_package', 'changed_package', 'deleted_package'.
|
|
||||||
|
|
||||||
"""
|
|
||||||
q = model.Session.query(Activity)
|
|
||||||
q = q.filter(Activity.activity_type.endswith("package"))
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def recently_changed_packages_activity_list(
|
|
||||||
limit: int, offset: int
|
|
||||||
) -> list[Activity]:
|
|
||||||
"""Return the site-wide stream of recently changed package activities.
|
|
||||||
|
|
||||||
This activity stream includes recent 'new package', 'changed package' and
|
|
||||||
'deleted package' activities for the whole site.
|
|
||||||
|
|
||||||
"""
|
|
||||||
q = _changed_packages_activity_query()
|
|
||||||
|
|
||||||
q = _filter_activitites_from_users(q)
|
|
||||||
|
|
||||||
return _activities_limit(q, limit, offset).all()
|
|
||||||
|
|
||||||
|
|
||||||
def _filter_activitites_from_users(q: QActivity) -> QActivity:
|
|
||||||
"""
|
|
||||||
Adds a filter to an existing query object to avoid activities from users
|
|
||||||
defined in :ref:`ckan.hide_activity_from_users` (defaults to the site user)
|
|
||||||
"""
|
|
||||||
users_to_avoid = _activity_stream_get_filtered_users()
|
|
||||||
if users_to_avoid:
|
|
||||||
# type_ignore_reason: incomplete SQLAlchemy types
|
|
||||||
q = q.filter(Activity.user_id.notin_(users_to_avoid)) # type: ignore
|
|
||||||
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def _filter_activitites_from_type(
|
|
||||||
q: QActivity, types: list[str], include: bool = True
|
|
||||||
):
|
|
||||||
"""Adds a filter to an existing query object to include or exclude
|
|
||||||
(include=False) activities based on a list of types.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if include:
|
|
||||||
q = q.filter(Activity.activity_type.in_(types)) # type: ignore
|
|
||||||
else:
|
|
||||||
q = q.filter(Activity.activity_type.notin_(types)) # type: ignore
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def _activity_stream_get_filtered_users() -> list[str]:
|
|
||||||
"""
|
|
||||||
Get the list of users from the :ref:`ckan.hide_activity_from_users` config
|
|
||||||
option and return a list of their ids. If the config is not specified,
|
|
||||||
returns the id of the site user.
|
|
||||||
"""
|
|
||||||
users_list = config.get("ckan.hide_activity_from_users")
|
|
||||||
if not users_list:
|
|
||||||
from ckan.logic import get_action
|
|
||||||
|
|
||||||
context: Context = {"ignore_auth": True}
|
|
||||||
site_user = get_action("get_site_user")(context, {})
|
|
||||||
users_list = [site_user.get("name")]
|
|
||||||
|
|
||||||
return model.User.user_ids_for_name_or_id(users_list)
|
|
|
@ -1,30 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from ckan.common import CKANConfig
|
|
||||||
|
|
||||||
import ckan.plugins as p
|
|
||||||
import ckan.plugins.toolkit as tk
|
|
||||||
|
|
||||||
from . import subscriptions
|
|
||||||
|
|
||||||
|
|
||||||
@tk.blanket.auth_functions
|
|
||||||
@tk.blanket.actions
|
|
||||||
@tk.blanket.helpers
|
|
||||||
@tk.blanket.blueprints
|
|
||||||
@tk.blanket.validators
|
|
||||||
class ActivityPlugin(p.SingletonPlugin):
|
|
||||||
p.implements(p.IConfigurer)
|
|
||||||
p.implements(p.ISignal)
|
|
||||||
|
|
||||||
# IConfigurer
|
|
||||||
def update_config(self, config: CKANConfig):
|
|
||||||
tk.add_template_directory(config, "templates")
|
|
||||||
tk.add_public_directory(config, "public")
|
|
||||||
tk.add_resource("assets", "ckanext-activity")
|
|
||||||
|
|
||||||
# ISignal
|
|
||||||
def get_signal_subscriptions(self):
|
|
||||||
return subscriptions.get_subscriptions()
|
|
Binary file not shown.
Before Width: | Height: | Size: 74 B |
|
@ -1,177 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import ckan.plugins.toolkit as tk
|
|
||||||
import ckan.lib.dictization as dictization
|
|
||||||
|
|
||||||
from ckan import types
|
|
||||||
from .model import Activity
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_subscriptions() -> types.SignalMapping:
|
|
||||||
return {
|
|
||||||
tk.signals.action_succeeded: [
|
|
||||||
{"sender": "bulk_update_public", "receiver": bulk_changed},
|
|
||||||
{"sender": "bulk_update_private", "receiver": bulk_changed},
|
|
||||||
{"sender": "bulk_update_delete", "receiver": bulk_changed},
|
|
||||||
{"sender": "package_create", "receiver": package_changed},
|
|
||||||
{"sender": "package_update", "receiver": package_changed},
|
|
||||||
{"sender": "package_delete", "receiver": package_changed},
|
|
||||||
{"sender": "group_create", "receiver": group_or_org_changed},
|
|
||||||
{"sender": "group_update", "receiver": group_or_org_changed},
|
|
||||||
{"sender": "group_delete", "receiver": group_or_org_changed},
|
|
||||||
{
|
|
||||||
"sender": "organization_create",
|
|
||||||
"receiver": group_or_org_changed,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sender": "organization_update",
|
|
||||||
"receiver": group_or_org_changed,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sender": "organization_delete",
|
|
||||||
"receiver": group_or_org_changed,
|
|
||||||
},
|
|
||||||
{"sender": "user_create", "receiver": user_changed},
|
|
||||||
{"sender": "user_update", "receiver": user_changed},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# action, context, data_dict, result
|
|
||||||
def bulk_changed(sender: str, **kwargs: Any):
|
|
||||||
for key in ("context", "data_dict"):
|
|
||||||
if key not in kwargs:
|
|
||||||
log.warning("Activity subscription ignored")
|
|
||||||
return
|
|
||||||
|
|
||||||
context: types.Context = kwargs["context"]
|
|
||||||
datasets = kwargs["data_dict"].get("datasets")
|
|
||||||
model = context["model"]
|
|
||||||
|
|
||||||
user = context["user"]
|
|
||||||
user_obj = model.User.get(user)
|
|
||||||
if user_obj:
|
|
||||||
user_id = user_obj.id
|
|
||||||
else:
|
|
||||||
user_id = "not logged in"
|
|
||||||
for dataset in datasets:
|
|
||||||
entity = model.Package.get(dataset)
|
|
||||||
assert entity
|
|
||||||
|
|
||||||
activity = Activity.activity_stream_item(entity, "changed", user_id)
|
|
||||||
model.Session.add(activity)
|
|
||||||
|
|
||||||
if not context.get("defer_commit"):
|
|
||||||
model.Session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
# action, context, data_dict, result
|
|
||||||
def package_changed(sender: str, **kwargs: Any):
|
|
||||||
for key in ("result", "context", "data_dict"):
|
|
||||||
if key not in kwargs:
|
|
||||||
log.warning("Activity subscription ignored")
|
|
||||||
return
|
|
||||||
|
|
||||||
type_ = "new" if sender == "package_create" else "changed"
|
|
||||||
|
|
||||||
context: types.Context = kwargs["context"]
|
|
||||||
result: types.ActionResult.PackageUpdate = kwargs["result"]
|
|
||||||
data_dict = kwargs["data_dict"]
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
id_ = data_dict["id"]
|
|
||||||
elif isinstance(result, str):
|
|
||||||
id_ = result
|
|
||||||
else:
|
|
||||||
id_ = result["id"]
|
|
||||||
|
|
||||||
pkg = context["model"].Package.get(id_)
|
|
||||||
assert pkg
|
|
||||||
|
|
||||||
if pkg.private:
|
|
||||||
return
|
|
||||||
|
|
||||||
user_obj = context["model"].User.get(context["user"])
|
|
||||||
if user_obj:
|
|
||||||
user_id = user_obj.id
|
|
||||||
else:
|
|
||||||
user_id = "not logged in"
|
|
||||||
|
|
||||||
activity = Activity.activity_stream_item(pkg, type_, user_id)
|
|
||||||
context["session"].add(activity)
|
|
||||||
if not context.get("defer_commit"):
|
|
||||||
context["session"].commit()
|
|
||||||
|
|
||||||
|
|
||||||
# action, context, data_dict, result
|
|
||||||
def group_or_org_changed(sender: str, **kwargs: Any):
|
|
||||||
for key in ("result", "context", "data_dict"):
|
|
||||||
if key not in kwargs:
|
|
||||||
log.warning("Activity subscription ignored")
|
|
||||||
return
|
|
||||||
|
|
||||||
context: types.Context = kwargs["context"]
|
|
||||||
result: types.ActionResult.GroupUpdate = kwargs["result"]
|
|
||||||
data_dict = kwargs["data_dict"]
|
|
||||||
|
|
||||||
group = context["model"].Group.get(
|
|
||||||
result["id"] if result else data_dict["id"]
|
|
||||||
)
|
|
||||||
assert group
|
|
||||||
|
|
||||||
type_, action = sender.split("_")
|
|
||||||
|
|
||||||
user_obj = context["model"].User.get(context["user"])
|
|
||||||
assert user_obj
|
|
||||||
|
|
||||||
activity_dict: dict[str, Any] = {
|
|
||||||
"user_id": user_obj.id,
|
|
||||||
"object_id": group.id,
|
|
||||||
}
|
|
||||||
|
|
||||||
if group.state == "deleted" or action == "delete":
|
|
||||||
activity_type = f"deleted {type_}"
|
|
||||||
elif action == "create":
|
|
||||||
activity_type = f"new {type_}"
|
|
||||||
else:
|
|
||||||
activity_type = f"changed {type_}"
|
|
||||||
|
|
||||||
activity_dict["activity_type"] = activity_type
|
|
||||||
|
|
||||||
activity_dict["data"] = {
|
|
||||||
"group": dictization.table_dictize(group, context)
|
|
||||||
}
|
|
||||||
activity_create_context = tk.fresh_context(context)
|
|
||||||
activity_create_context['ignore_auth'] = True
|
|
||||||
tk.get_action("activity_create")(activity_create_context, activity_dict)
|
|
||||||
|
|
||||||
|
|
||||||
# action, context, data_dict, result
|
|
||||||
def user_changed(sender: str, **kwargs: Any):
|
|
||||||
for key in ("result", "context", "data_dict"):
|
|
||||||
if key not in kwargs:
|
|
||||||
log.warning("Activity subscription ignored")
|
|
||||||
return
|
|
||||||
|
|
||||||
context: types.Context = kwargs["context"]
|
|
||||||
result: types.ActionResult.UserUpdate = kwargs["result"]
|
|
||||||
|
|
||||||
if sender == "user_create":
|
|
||||||
activity_type = "new user"
|
|
||||||
else:
|
|
||||||
activity_type = "changed user"
|
|
||||||
|
|
||||||
activity_dict = {
|
|
||||||
"user_id": result["id"],
|
|
||||||
"object_id": result["id"],
|
|
||||||
"activity_type": activity_type,
|
|
||||||
}
|
|
||||||
activity_create_context = tk.fresh_context(context)
|
|
||||||
activity_create_context['ignore_auth'] = True
|
|
||||||
tk.get_action("activity_create")(activity_create_context, activity_dict)
|
|
|
@ -1,7 +0,0 @@
|
||||||
{% set num = activities|length %}{{ ngettext("You have {num} new activity on your {site_title} dashboard", "You have {num} new activities on your {site_title} dashboard", num).format(site_title=g.site_title if g else site_title, num=num) }} {{ _('To view your dashboard, click on this link:') }}
|
|
||||||
|
|
||||||
{% url_for 'activity.dashboard', _external=True %}
|
|
||||||
|
|
||||||
{{ _('You can turn off these email notifications in your {site_title} preferences. To change your preferences, click on this link:').format(site_title=g.site_title if g else site_title) }}
|
|
||||||
|
|
||||||
{% url_for 'user.edit', _external=True %}
|
|
|
@ -1,4 +0,0 @@
|
||||||
{# Snippet for unit testing dashboard.js #}
|
|
||||||
<div>
|
|
||||||
{% snippet 'user/snippets/followee_dropdown.html', context={}, followees=[{"dict": {"id": 1}, "display_name": "Test followee" }, {"dict": {"id": 2}, "display_name": "Not valid" }] %}
|
|
||||||
</div>
|
|
|
@ -1,6 +0,0 @@
|
||||||
{% ckan_extends %}
|
|
||||||
|
|
||||||
{% block styles %}
|
|
||||||
{{ super() }}
|
|
||||||
{% asset 'ckanext-activity/activity-css' %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,15 +0,0 @@
|
||||||
{% extends "group/read_base.html" %}
|
|
||||||
|
|
||||||
{% block subtitle %}{{ _('Activity Stream') }} {{ g.template_title_delimiter }} {{ super() }}{% endblock %}
|
|
||||||
|
|
||||||
{% block primary_content_inner %}
|
|
||||||
|
|
||||||
{% if activity_types is defined %}
|
|
||||||
{% snippet 'snippets/activity_type_selector.html', id=id, activity_type=activity_type, activity_types=activity_types, blueprint='activity.group_activity' %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% snippet 'snippets/stream.html', activity_stream=activity_stream, id=id, object_type='group' %}
|
|
||||||
|
|
||||||
{% snippet 'snippets/pagination.html', newer_activities_url=newer_activities_url, older_activities_url=older_activities_url %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,64 +0,0 @@
|
||||||
{% extends "group/read_base.html" %}
|
|
||||||
|
|
||||||
{% block subtitle %}{{ group_dict.name }} {{ g.template_title_delimiter }} Changes {{ g.template_title_delimiter }} {{ super() }}{% endblock %}
|
|
||||||
|
|
||||||
{% block breadcrumb_content_selected %}{% endblock %}
|
|
||||||
|
|
||||||
{% block breadcrumb_content %}
|
|
||||||
{{ super() }}
|
|
||||||
<li>{% link_for _('Changes'), named_route='activity.group_activity', id=group_dict.name %}</li>
|
|
||||||
<li class="active">{% link_for activity_diffs[0].activities[1].id|truncate(30), named_route='activity.group_changes', id=activity_diffs[0].activities[1].id %}</li>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block primary %}
|
|
||||||
<article class="module">
|
|
||||||
<div class="module-content">
|
|
||||||
{% block group_changes_header %}
|
|
||||||
<h1 class="page-heading">{{ _('Changes') }}</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% set select_list1 = h.activity_list_select(group_activity_list, activity_diffs[-1].activities[0].id) %}
|
|
||||||
{% set select_list2 = h.activity_list_select(group_activity_list, activity_diffs[0].activities[1].id) %}
|
|
||||||
<form id="range_form" action="{{ h.url_for('activity.group_changes_multiple') }}" data-module="select-switch" data-module-target="">
|
|
||||||
<input type="hidden" name="current_old_id" value="{{ activity_diffs[-1].activities[0].id }}">
|
|
||||||
<input type="hidden" name="current_new_id" value="{{ activity_diffs[0].activities[1].id }}">
|
|
||||||
View changes from
|
|
||||||
<select class="form-control select-time" form="range_form" name="old_id">
|
|
||||||
<pre>
|
|
||||||
{{ select_list1[1:]|join }}
|
|
||||||
</pre>
|
|
||||||
</select> to
|
|
||||||
<select class="form-control select-time" form="range_form" name="new_id">
|
|
||||||
<pre>
|
|
||||||
{{ select_list2|join }}
|
|
||||||
</pre>
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
{# iterate through the list of activity diffs #}
|
|
||||||
<hr>
|
|
||||||
{% for i in range(activity_diffs|length) %}
|
|
||||||
{% snippet "group/snippets/item_group.html", activity_diff=activity_diffs[i], group_dict=group_dict %}
|
|
||||||
|
|
||||||
{# TODO: display metadata for more than most recent change #}
|
|
||||||
{% if i == 0 %}
|
|
||||||
{# button to show JSON metadata diff for the most recent change - not shown by default #}
|
|
||||||
<input type="button" data-module="metadata-button" data-module-target="" class="btn" value="Show metadata diff" id="metadata_button"></input>
|
|
||||||
<div id="metadata_diff" style="display:none;">
|
|
||||||
{% block group_changes_diff %}
|
|
||||||
<pre>
|
|
||||||
{{ activity_diffs[0]['diff']|safe }}
|
|
||||||
</pre>
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block secondary %}{% endblock %}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{% ckan_extends %}
|
|
||||||
|
|
||||||
{% block content_primary_nav %}
|
|
||||||
{{ super() }}
|
|
||||||
{{ h.build_nav_icon('activity.group_activity', _('Activity Stream'), id=group_dict.name, offset=0, icon='clock') }}
|
|
||||||
{% endblock content_primary_nav %}
|
|
|
@ -1,10 +0,0 @@
|
||||||
<strong>{{ gettext('On %(timestamp)s, %(username)s:', timestamp=h.render_datetime(activity_diff.activities[1].timestamp, with_hours=True, with_seconds=True), username=h.linked_user(activity_diff.activities[1].user_id)) }}</strong>
|
|
||||||
|
|
||||||
{% set changes = h.compare_group_dicts(activity_diff.activities[0].data.group, activity_diff.activities[1].data.group, activity_diff.activities[0].id) %}
|
|
||||||
<ul>
|
|
||||||
{% for change in changes %}
|
|
||||||
{% snippet "snippets/group_changes/{}.html".format(
|
|
||||||
change.type), change=change %}
|
|
||||||
<br>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
|
@ -1,15 +0,0 @@
|
||||||
{% ckan_extends %}
|
|
||||||
|
|
||||||
{% block header_dashboard %}
|
|
||||||
{% set new_activities = h.new_activities() %}
|
|
||||||
<li class="notifications {% if new_activities > 0 %}notifications-important{% endif %}">
|
|
||||||
{% set notifications_tooltip = ngettext('Dashboard (%(num)d new item)', 'Dashboard (%(num)d new items)',
|
|
||||||
new_activities)
|
|
||||||
%}
|
|
||||||
<a href="{{ h.url_for('activity.dashboard') }}" title="{{ notifications_tooltip }}">
|
|
||||||
<i class="fa fa-tachometer" aria-hidden="true"></i>
|
|
||||||
<span class="text">{{ _('Dashboard') }}</span>
|
|
||||||
<span class="badge">{{ new_activities }}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endblock %}
|
|
|
@ -1,14 +0,0 @@
|
||||||
{% extends "organization/read_base.html" %}
|
|
||||||
|
|
||||||
{% block subtitle %}{{ _('Activity Stream') }} {{ g.template_title_delimiter }} {{ super() }}{% endblock %}
|
|
||||||
|
|
||||||
{% block primary_content_inner %}
|
|
||||||
|
|
||||||
{% if activity_types is defined %}
|
|
||||||
{% snippet 'snippets/activity_type_selector.html', id=id, activity_type=activity_type, activity_types=activity_types, blueprint='activity.organization_activity' %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% snippet 'snippets/stream.html', activity_stream=activity_stream, id=id, object_type='organization', group_type=group_type %}
|
|
||||||
|
|
||||||
{% snippet 'snippets/pagination.html', newer_activities_url=newer_activities_url, older_activities_url=older_activities_url %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,64 +0,0 @@
|
||||||
{% extends "organization/read_base.html" %}
|
|
||||||
|
|
||||||
{% block subtitle %}{{ group_dict.name }} {{ g.template_title_delimiter }} Changes {{ g.template_title_delimiter }} {{ super() }}{% endblock %}
|
|
||||||
|
|
||||||
{% block breadcrumb_content_selected %}{% endblock %}
|
|
||||||
|
|
||||||
{% block breadcrumb_content %}
|
|
||||||
{{ super() }}
|
|
||||||
<li>{% link_for _('Changes'), named_route='activity.organization_activity', id=group_dict.name %}</li>
|
|
||||||
<li class="active">{% link_for activity_diffs[0].activities[1].id|truncate(30), named_route='activity.organization_changes', id=activity_diffs[0].activities[1].id %}</li>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block primary %}
|
|
||||||
<article class="module">
|
|
||||||
<div class="module-content">
|
|
||||||
{% block organization_changes_header %}
|
|
||||||
<h1 class="page-heading">{{ _('Changes') }}</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% set select_list1 = h.activity_list_select(group_activity_list, activity_diffs[-1].activities[0].id) %}
|
|
||||||
{% set select_list2 = h.activity_list_select(group_activity_list, activity_diffs[0].activities[1].id) %}
|
|
||||||
<form id="range_form" action="{{ h.url_for('activity.organization_changes_multiple') }}" data-module="select-switch" data-module-target="">
|
|
||||||
<input type="hidden" name="current_old_id" value="{{ activity_diffs[-1].activities[0].id }}">
|
|
||||||
<input type="hidden" name="current_new_id" value="{{ activity_diffs[0].activities[1].id }}">
|
|
||||||
View changes from
|
|
||||||
<select class="form-control select-time" form="range_form" name="old_id">
|
|
||||||
<pre>
|
|
||||||
{{ select_list1[1:]|join }}
|
|
||||||
</pre>
|
|
||||||
</select> to
|
|
||||||
<select class="form-control select-time" form="range_form" name="new_id">
|
|
||||||
<pre>
|
|
||||||
{{ select_list2|join }}
|
|
||||||
</pre>
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
{# iterate through the list of activity diffs #}
|
|
||||||
<hr>
|
|
||||||
{% for i in range(activity_diffs|length) %}
|
|
||||||
{% snippet "organization/snippets/item_organization.html", activity_diff=activity_diffs[i], group_dict=group_dict %}
|
|
||||||
|
|
||||||
{# TODO: display metadata for more than most recent change #}
|
|
||||||
{% if i == 0 %}
|
|
||||||
{# button to show JSON metadata diff for the most recent change - not shown by default #}
|
|
||||||
<input type="button" data-module="metadata-button" data-module-target="" class="btn" value="Show metadata diff" id="metadata_button"></input>
|
|
||||||
<div id="metadata_diff" style="display:none;">
|
|
||||||
{% block organization_changes_diff %}
|
|
||||||
<pre>
|
|
||||||
{{ activity_diffs[0]['diff']|safe }}
|
|
||||||
</pre>
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block secondary %}{% endblock %}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{% ckan_extends %}
|
|
||||||
|
|
||||||
{% block content_primary_nav %}
|
|
||||||
{{ super() }}
|
|
||||||
{{ h.build_nav_icon('activity.organization_activity', _('Activity Stream'), id=group_dict.name, offset=0, icon='clock') }}
|
|
||||||
{% endblock content_primary_nav %}
|
|
|
@ -1,10 +0,0 @@
|
||||||
<strong>{{ gettext('On %(timestamp)s, %(username)s:', timestamp=h.render_datetime(activity_diff.activities[1].timestamp, with_hours=True, with_seconds=True), username=h.linked_user(activity_diff.activities[1].user_id)) }}</strong>
|
|
||||||
|
|
||||||
{% set changes = h.compare_group_dicts(activity_diff.activities[0].data.group, activity_diff.activities[1].data.group, activity_diff.activities[0].id) %}
|
|
||||||
<ul>
|
|
||||||
{% for change in changes %}
|
|
||||||
{% snippet "snippets/organization_changes/{}.html".format(
|
|
||||||
change.type), change=change %}
|
|
||||||
<br>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
|
@ -1,24 +0,0 @@
|
||||||
{% extends "package/read_base.html" %}
|
|
||||||
|
|
||||||
{% block subtitle %}{{ _('Activity Stream') }} {{ g.template_title_delimiter }} {{ super() }}{% endblock %}
|
|
||||||
|
|
||||||
{% block primary_content_inner %}
|
|
||||||
{% if activity_types is defined %}
|
|
||||||
{% snippet 'snippets/activity_type_selector.html', id=id, activity_type=activity_type, activity_types=activity_types, blueprint='dataset.activity' %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if activity_stream|length > 0 %}
|
|
||||||
{% snippet 'snippets/stream.html', activity_stream=activity_stream, id=id, object_type='package' %}
|
|
||||||
{% else %}
|
|
||||||
<p>
|
|
||||||
{% if activity_type %}
|
|
||||||
{{ _('No activity found for this type') }}
|
|
||||||
{% else %}
|
|
||||||
{{ _('No activity found') }}.
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% snippet 'snippets/pagination.html', newer_activities_url=newer_activities_url, older_activities_url=older_activities_url %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,64 +0,0 @@
|
||||||
{% extends "package/base.html" %}
|
|
||||||
|
|
||||||
{% block subtitle %}{{ pkg_dict.name }} {{ g.template_title_delimiter }} Changes {{ g.template_title_delimiter }} {{ super() }}{% endblock %}
|
|
||||||
|
|
||||||
{% block breadcrumb_content_selected %}{% endblock %}
|
|
||||||
|
|
||||||
{% block breadcrumb_content %}
|
|
||||||
{{ super() }}
|
|
||||||
<li>{% link_for _('Changes'), named_route='activity.package_activity', id=pkg_dict.name %}</li>
|
|
||||||
<li class="active">{% link_for activity_diffs[0].activities[1].id|truncate(30), named_route='activity.package_changes', id=activity_diffs[0].activities[1].id %}</li>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block primary %}
|
|
||||||
<article class="module">
|
|
||||||
<div class="module-content">
|
|
||||||
{% block package_changes_header %}
|
|
||||||
<h1 class="page-heading">{{ _('Changes') }}</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% set select_list1 = h.activity_list_select(pkg_activity_list, activity_diffs[-1].activities[0].id) %}
|
|
||||||
{% set select_list2 = h.activity_list_select(pkg_activity_list, activity_diffs[0].activities[1].id) %}
|
|
||||||
<form id="range_form" action="{{ h.url_for('activity.package_changes_multiple') }}" data-module="select-switch" data-module-target="">
|
|
||||||
<input type="hidden" name="current_old_id" value="{{ activity_diffs[-1].activities[0].id }}">
|
|
||||||
<input type="hidden" name="current_new_id" value="{{ activity_diffs[0].activities[1].id }}">
|
|
||||||
View changes from
|
|
||||||
<select class="form-control select-time" form="range_form" name="old_id">
|
|
||||||
<pre>
|
|
||||||
{{ select_list1[1:]|join }}
|
|
||||||
</pre>
|
|
||||||
</select> to
|
|
||||||
<select class="form-control select-time" form="range_form" name="new_id">
|
|
||||||
<pre>
|
|
||||||
{{ select_list2|join }}
|
|
||||||
</pre>
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
{# iterate through the list of activity diffs #}
|
|
||||||
<hr>
|
|
||||||
{% for i in range(activity_diffs|length) %}
|
|
||||||
{% snippet "package/snippets/change_item.html", activity_diff=activity_diffs[i], pkg_dict=pkg_dict %}
|
|
||||||
|
|
||||||
{# TODO: display metadata for more than most recent change #}
|
|
||||||
{% if i == 0 %}
|
|
||||||
{# button to show JSON metadata diff for the most recent change - not shown by default #}
|
|
||||||
<input type="button" data-module="metadata-button" data-module-target="" class="btn" value="Show metadata diff" id="metadata_button"></input>
|
|
||||||
<div id="metadata_diff" style="display:none;">
|
|
||||||
{% block package_changes_diff %}
|
|
||||||
<pre>
|
|
||||||
{{ activity_diffs[0]['diff']|safe }}
|
|
||||||
</pre>
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block secondary %}{% endblock %}
|
|
|
@ -1,23 +0,0 @@
|
||||||
{% extends "package/read.html" %}
|
|
||||||
|
|
||||||
{% block package_description %}
|
|
||||||
{% block package_archive_notice %}
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
{% trans url=h.url_for(pkg.type ~ '.read', id=pkg.id) %}
|
|
||||||
You're currently viewing an old version of this dataset.
|
|
||||||
Data files may not match the old version of the metadata.
|
|
||||||
<a href="{{ url }}">View the current version</a>.
|
|
||||||
{% endtrans %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock package_description %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block package_resources %}
|
|
||||||
{% snippet "package/snippets/resources_list.html", pkg=pkg, resources=pkg.resources, is_activity_archive=true %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content_action %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{% ckan_extends %}
|
|
||||||
|
|
||||||
{% block content_primary_nav %}
|
|
||||||
{{ super() }}
|
|
||||||
{{ h.build_nav_icon('activity.package_activity', _('Activity Stream'), id=pkg.id if is_activity_archive else pkg.name, icon='clock') }}
|
|
||||||
{% endblock content_primary_nav %}
|
|
|
@ -1,26 +0,0 @@
|
||||||
{% extends "package/resource_read.html" %}
|
|
||||||
|
|
||||||
{% block action_manage %}
|
|
||||||
{% endblock action_manage %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block resource_content %}
|
|
||||||
{% block package_archive_notice %}
|
|
||||||
<div id="activity-archive-notice" class="alert alert-danger">
|
|
||||||
{% trans url=h.url_for(pkg.type ~ '.read', id=pkg.id) %}
|
|
||||||
You're currently viewing an old version of this dataset.
|
|
||||||
Data files may not match the old version of the metadata.
|
|
||||||
<a href="{{ url }}">View the current version</a>.
|
|
||||||
{% endtrans %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block data_preview %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block resources_list %}
|
|
||||||
{% snippet "package/snippets/resources.html", pkg=pkg, active=res.id, action='read', is_activity_archive=true %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,10 +0,0 @@
|
||||||
<strong>{{ gettext('On %(timestamp)s, %(username)s:', timestamp=h.render_datetime(activity_diff.activities[1].timestamp, with_hours=True, with_seconds=True), username=h.linked_user(activity_diff.activities[1].user_id)) }}</strong>
|
|
||||||
|
|
||||||
{% set changes = h.compare_pkg_dicts(activity_diff.activities[0].data.package, activity_diff.activities[1].data.package, activity_diff.activities[0].id) %}
|
|
||||||
<ul>
|
|
||||||
{% for change in changes %}
|
|
||||||
{% snippet "snippets/changes/{}.html".format(
|
|
||||||
change.type), change=change, pkg_dict=pkg_dict %}
|
|
||||||
<br>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
|
@ -1,16 +0,0 @@
|
||||||
{% ckan_extends %}
|
|
||||||
|
|
||||||
{% block explore_view %}
|
|
||||||
{% if is_activity_archive %}
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="{{ url }}">
|
|
||||||
<i class="fa fa-info-circle"></i>
|
|
||||||
{{ _('More information') }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
{{ super() }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock explore_view %}
|
|
|
@ -1,12 +0,0 @@
|
||||||
{% ckan_extends %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block resources_list %}
|
|
||||||
<ul class="list-unstyled nav nav-simple">
|
|
||||||
{% for resource in resources %}
|
|
||||||
<li class="nav-item{{ ' active' if active == resource.id }}">
|
|
||||||
<a href="{{ h.url_for("activity.resource_history" if is_activity_archive else '%s_resource.%s' % (pkg.type, (action or 'read')), id=pkg.id if is_activity_archive else pkg.name, resource_id=resource.id, inner_span=true, **({'activity_id': request.view_args.activity_id} if is_activity_archive else {})) }}" title="{{ h.resource_display_name(resource) }}">{{ h.resource_display_name(resource)|truncate(25) }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
|
@ -1,15 +0,0 @@
|
||||||
{% ckan_extends %}
|
|
||||||
|
|
||||||
{% block resource_list_inner %}
|
|
||||||
{% for resource in resources %}
|
|
||||||
{% if is_activity_archive %}
|
|
||||||
{% set url = h.url_for("activity.resource_history", id=pkg.id, resource_id=resource.id, activity_id=request.view_args.activity_id) %}
|
|
||||||
{% endif %}
|
|
||||||
{% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, can_edit=false if is_activity_archive else can_edit, is_activity_archive=is_activity_archive, url=url %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block resource_list_empty %}
|
|
||||||
<p class="empty">{{ _('This dataset has no data') }}</p>
|
|
||||||
{% endblock %}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{% ckan_extends %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
{{ super() }}
|
|
||||||
{% asset "ckanext-activity/activity" %}
|
|
||||||
{% endblock scripts %}
|
|
|
@ -1,15 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-tag fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} added the tag {tag} to the dataset {dataset}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
dataset=ah.dataset(activity),
|
|
||||||
tag=ah.tag(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,20 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-users fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} updated the group {group}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
group=ah.group(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
{% if can_show_activity_detail %}
|
|
||||||
|
|
|
||||||
<a href="{{ h.url_for('activity.group_changes', id=activity.id) }}">
|
|
||||||
{{ _('Changes') }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,20 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-briefcase fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} updated the organization {organization}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
organization=ah.organization(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
{% if can_show_activity_detail %}
|
|
||||||
|
|
|
||||||
<a href="{{ h.url_for('activity.organization_changes', id=activity.id) }}">
|
|
||||||
{{ _('Changes') }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,26 +0,0 @@
|
||||||
{% set dataset_type = activity.data.package.type or 'dataset' %}
|
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-sitemap fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} updated the {dataset_type} {dataset}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
dataset_type=h.humanize_entity_type('package', dataset_type, 'activity_record') or _('dataset'),
|
|
||||||
dataset=ah.dataset(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
{% if can_show_activity_detail %}
|
|
||||||
|
|
|
||||||
<a href="{{ h.url_for('activity.package_history', id=activity.object_id, activity_id=activity.id) }}">
|
|
||||||
{{ _('View this version') }}
|
|
||||||
</a>
|
|
||||||
|
|
|
||||||
<a href="{{ h.url_for('activity.package_changes', id=activity.id) }}">
|
|
||||||
{{ _('Changes') }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-icon fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} updated the resource {resource} in the dataset {dataset}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
resource=ah.resource(activity),
|
|
||||||
dataset=ah.datset(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-users fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} updated their profile').format(
|
|
||||||
actor=ah.actor(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,14 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-users fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} deleted the group {group}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
group=ah.group(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,14 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-briefcase fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} deleted the organization {organization}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
organization=ah.organization(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,17 +0,0 @@
|
||||||
{% set dataset_type = activity.data.package.type or 'dataset' %}
|
|
||||||
|
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-sitemap fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} deleted the {dataset_type} {dataset}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
dataset_type=h.humanize_entity_type('package', dataset_type, 'activity_record') or _('dataset'),
|
|
||||||
dataset=ah.dataset(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-file fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} deleted the resource {resource} from the dataset {dataset}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
resource=ah.resource(activity),
|
|
||||||
dataset=ah.dataset(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,38 +0,0 @@
|
||||||
{#
|
|
||||||
Fallback template for displaying an activity.
|
|
||||||
It's not pretty, but it is better than TemplateNotFound.
|
|
||||||
|
|
||||||
Params:
|
|
||||||
activity - the Activity dict
|
|
||||||
can_show_activity_detail - whether we should render detail about the activity (i.e. "as it was" and diff, alternatively will just display the metadata about the activity)
|
|
||||||
id - the id or current name of the object (e.g. package name, user id)
|
|
||||||
ah - dict of template macros to render linked: actor, dataset, organization, user, group
|
|
||||||
#}
|
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-users fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} {activity_type}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
activity_type=activity.activity_type
|
|
||||||
)|safe }}
|
|
||||||
{% if activity.data.package %}
|
|
||||||
{{ ah.dataset(activity) }}
|
|
||||||
{% endif %}
|
|
||||||
{% if activity.data.package %}
|
|
||||||
{{ ah.dataset(activity) }}
|
|
||||||
{% endif %}
|
|
||||||
{% if activity.data.group %}
|
|
||||||
{# do our best to differentiate between org & group #}
|
|
||||||
{% if 'group' in activity.activity_type %}
|
|
||||||
{{ ah.group(activity) }}
|
|
||||||
{% else %}
|
|
||||||
{{ ah.organization(activity) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,14 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-sitemap fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} started following {dataset}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
dataset=ah.dataset(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,14 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-users fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} started following {group}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
group=ah.group(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,14 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-users fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} started following {user}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
user=ah.user(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,14 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-users fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} created the group {group}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
group=ah.group(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,14 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-briefcase fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} created the organization {organization}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
organization=ah.organization(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,22 +0,0 @@
|
||||||
{% set dataset_type = activity.data.package.type or 'dataset' %}
|
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-sitemap fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} created the {dataset_type} {dataset}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
dataset_type=h.humanize_entity_type('package', dataset_type,'activity_record') or _('dataset'),
|
|
||||||
dataset=ah.dataset(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
{% if can_show_activity_detail %}
|
|
||||||
|
|
|
||||||
<a href="{{ h.url_for('activity.package_history', id=activity.object_id, activity_id=activity.id) }}">
|
|
||||||
{{ _('View this version') }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-file fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} added the resource {resource} to the dataset {dataset}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
resource=ah.resource(activity),
|
|
||||||
dataset=ah.dataset(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-user fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} signed up').format(
|
|
||||||
actor=ah.actor(activity)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
|
|
||||||
<span class="fa-stack fa-lg">
|
|
||||||
<i class="fa fa-circle fa-stack-2x icon"></i>
|
|
||||||
<i class="fa fa-tag fa-stack-1x fa-inverse"></i>
|
|
||||||
</span>
|
|
||||||
{{ _('{actor} removed the tag {tag} from the dataset {dataset}').format(
|
|
||||||
actor=ah.actor(activity),
|
|
||||||
tag=ah.tag(activity),
|
|
||||||
dataset=ah.dataset(dataset)
|
|
||||||
)|safe }}
|
|
||||||
<br />
|
|
||||||
<span class="date" title="{{ h.render_datetime(activity.timestamp, with_hours=True) }}">
|
|
||||||
{{ h.time_ago_from_timestamp(activity.timestamp) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
|
@ -1,25 +0,0 @@
|
||||||
{#
|
|
||||||
Renders a select element to filter the activity stream
|
|
||||||
|
|
||||||
It uses the activity-stream.js module to dinamically request for a new URl upon selection.
|
|
||||||
|
|
||||||
id - the id or current name of the object (e.g. package name, user id)
|
|
||||||
activity_type - the current selected activity type
|
|
||||||
activity_types - the list of activity types the user can filter on
|
|
||||||
blueprint - blueprint to call when selecting an activity type to filter by (eg: dataset.activity)
|
|
||||||
#}
|
|
||||||
|
|
||||||
<div id="activity_types_filter" style="margin-bottom: 15px;" data-module="activity-stream">
|
|
||||||
<label for="activity_types_filter_select">Activity type</label>
|
|
||||||
<select id="activity_types_filter_select">
|
|
||||||
<option {% if not activity_types %}selected{% endif %} data-url="{{ h.url_for(blueprint, id=id) }}">
|
|
||||||
{{ _('All activity types') }}
|
|
||||||
</option>
|
|
||||||
{% for type_ in activity_types %}
|
|
||||||
<option {% if activity_type == type_ %}selected{% endif %} data-url="{{ h.url_for(blueprint, id=id, activity_type=type_) }}">
|
|
||||||
{# TODO: Calling humanize gets complex when displaying orgs/groups since the activities also contains dataset activities #}
|
|
||||||
{{ type_ }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% if change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Set author of {pkg_link} to {new_author} (previously {old_author})').format(
|
|
||||||
pkg_link=h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
new_author = change.new_author,
|
|
||||||
old_author = change.old_author
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "add" %}
|
|
||||||
|
|
||||||
{{ _('Set author of {pkg_link} to {new_author}').format(
|
|
||||||
pkg_link=h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
new_author = change.new_author
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "remove" %}
|
|
||||||
|
|
||||||
{{ _('Removed author from {pkg_link}').format(
|
|
||||||
pkg_link=h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id)
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,47 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% set pkg_link = (h.url_for('dataset.read', id=change.pkg_id))%}
|
|
||||||
{% if change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Set author email of ')}}
|
|
||||||
<a href="{{pkg_link}}">
|
|
||||||
{{ change.title }}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{ 'mailto:' + change.new_author_email }}">
|
|
||||||
{{change.new_author_email}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{_('( previously ')}}
|
|
||||||
<a href="{{ 'mailto:' + change.old_author_email }}">
|
|
||||||
{{change.old_author_email}}
|
|
||||||
</a>
|
|
||||||
{{ _(')') }}
|
|
||||||
|
|
||||||
{% elif change.method == "add" %}
|
|
||||||
|
|
||||||
{{_('Set author email of')}}
|
|
||||||
<a href="{{pkg_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{ 'mailto:' + change.new_author_email }}">
|
|
||||||
{{ change.new_author_email }}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% elif change.method == "remove" %}
|
|
||||||
|
|
||||||
{{ _('Removed author email from')}}
|
|
||||||
<a href="{{pkg_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{{ _('Deleted resource {resource_link} from {pkg_link}').format(
|
|
||||||
resource_link = h.nav_link(
|
|
||||||
change.resource_name, named_route='resource.read', qualified=True, id=change.pkg_id,
|
|
||||||
resource_id = change.resource_id, activity_id=change.old_activity_id),
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
) }}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{{ _('Changed value of field <q>{key}</q> to <q>{value}</q> in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
key = change.key,
|
|
||||||
value = change.value
|
|
||||||
)}}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,85 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% if change.method == "add_one_value" %}
|
|
||||||
|
|
||||||
{{ _('Added field <q>{key}</q> with value <q>{value}</q> to {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
key = change.key,
|
|
||||||
value = change.value
|
|
||||||
)}}
|
|
||||||
|
|
||||||
{% elif change.method == "add_one_no_value" %}
|
|
||||||
|
|
||||||
{{ _('Added field <q>{key}</q> to {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
key = change.key
|
|
||||||
)}}
|
|
||||||
|
|
||||||
{% elif change.method == "add_multiple" %}
|
|
||||||
|
|
||||||
{{ _('Added the following fields to {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id)
|
|
||||||
)}}
|
|
||||||
<ul>
|
|
||||||
{% for item in change.key_list %}
|
|
||||||
<li>
|
|
||||||
{% if change.value_list[item] != "" %}
|
|
||||||
{{ _('{key} with value {value}').format(
|
|
||||||
key = item,
|
|
||||||
value = change.value_list[item]
|
|
||||||
)|safe }}
|
|
||||||
{% else %}
|
|
||||||
{{ _('{key}').format(
|
|
||||||
key = item
|
|
||||||
)|safe }}
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{% elif change.method == "change_with_old_value" %}
|
|
||||||
|
|
||||||
{{ _('Changed value of field <q>{key}</q> to <q>{new_val}</q> (previously <q>{old_val}</q>) in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
key = change.key,
|
|
||||||
new_val = change.new_value,
|
|
||||||
old_val = change.old_value
|
|
||||||
)}}
|
|
||||||
|
|
||||||
{% elif change.method == "change_no_old_value" %}
|
|
||||||
|
|
||||||
{{ _('Changed value of field <q>{key}</q> to <q>{new_val}</q> in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
key = change.key,
|
|
||||||
new_val = change.new_value,
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "remove_one" %}
|
|
||||||
|
|
||||||
{{ _('Removed field <q>{key}</q> from {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
key = change.key,
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "remove_multiple" %}
|
|
||||||
|
|
||||||
{{ _('Removed the following fields from {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id)
|
|
||||||
) }}
|
|
||||||
<ul>
|
|
||||||
{% for item in change.key_list %}
|
|
||||||
<li>
|
|
||||||
{{ _('<q>{key}</q>').format(
|
|
||||||
key = item
|
|
||||||
)| safe }}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,67 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% set pkg_link = (h.url_for('dataset.read', id=change.pkg_id)) %}
|
|
||||||
{# if both of them have URLs #}
|
|
||||||
{% if change.new_url != "" and change.old_url != "" %}
|
|
||||||
|
|
||||||
{{ _('Changed the license of ') }}
|
|
||||||
<a href="{{pkg_link}}">
|
|
||||||
{{ change.title }}
|
|
||||||
<a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{change.new_url}}">
|
|
||||||
{{change.new_title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('( previously') }}
|
|
||||||
<a href="{{change.old_url}}">
|
|
||||||
{{change.old_title}}
|
|
||||||
</a>
|
|
||||||
{{_(')')}}
|
|
||||||
|
|
||||||
{# if only the new one has a URL #}
|
|
||||||
{% elif change.new_url != "" and change.old_url == "" %}
|
|
||||||
|
|
||||||
{{ _('Changed the license of') }}
|
|
||||||
<a href="{{pkg_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{change.new_url}}">
|
|
||||||
{{change.new_title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('(previously') + change.old_title + ' )'}}
|
|
||||||
|
|
||||||
{# if only the old one has a URL #}
|
|
||||||
{% elif change.new_url == "" and change.old_url != "" %}
|
|
||||||
|
|
||||||
{{ _('Changed the license of') }}
|
|
||||||
<a href="{{pkg_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') + change.new_title }}
|
|
||||||
|
|
||||||
{{ _('( previously') }}
|
|
||||||
<a href="{{change.old_url}}">
|
|
||||||
{{change.old_title}}
|
|
||||||
</a>
|
|
||||||
{{_(')')}}
|
|
||||||
|
|
||||||
{# otherwise neither has a URL #}
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('Changed the license of') }}
|
|
||||||
<a href="{{pkg_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') + change.new_title}}
|
|
||||||
{{ _('(previously') + change.old_title + _(')')|safe }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% if change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Set maintainer of {pkg_link} to {new_maintainer} (previously {old_maintainer})').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
new_maintainer = change.new_maintainer,
|
|
||||||
old_maintainer = change.old_maintainer
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "add" %}
|
|
||||||
|
|
||||||
{{ _('Set maintainer of {pkg_link} to {new_maintainer}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
new_maintainer = change.new_maintainer
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "remove" %}
|
|
||||||
|
|
||||||
{{ _('Removed maintainer from {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,47 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% set pkg_link = (h.url_for('dataset.read', id=change.pkg_id)) %}
|
|
||||||
{% if change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Set maintainer email of') }}
|
|
||||||
<a href="{{pkg_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{'mailto:' + change.new_maintainer_email}}">
|
|
||||||
{{change.new_maintainer_email}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('(previously') }}
|
|
||||||
<a href="{{'mailto:' + change.old_maintainer_email}}">
|
|
||||||
{{change.old_maintainer_email}}
|
|
||||||
</a>
|
|
||||||
{{ _(')') }}
|
|
||||||
|
|
||||||
{% elif change.method == "add" %}
|
|
||||||
|
|
||||||
{{ _('Set maintainer email of') }}
|
|
||||||
<a href="{{pkg_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{'mailto:' + change.new_maintainer_email}}">
|
|
||||||
{{change.new_maintainer_email}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% elif change.method == "remove" %}
|
|
||||||
|
|
||||||
{{ _('Removed maintainer email from') }}
|
|
||||||
<a href="{{pkg_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,23 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% set old_url = h.url_for('dataset.read', qualified=True, id=change.old_name) %}
|
|
||||||
{% set new_url = h.url_for('dataset.read', qualified=True, id=change.new_name) %}
|
|
||||||
|
|
||||||
{{ _('Moved')}}
|
|
||||||
<a href="{{ h.url_for('dataset.read', id=change.pkg_id) }}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('from') }}
|
|
||||||
<a href="{{old_url}}">
|
|
||||||
{{old_url}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{new_url}}">
|
|
||||||
{{new_url}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{{ _('Uploaded a new file to resource {resource_link} in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
resource_link = h.nav_link(
|
|
||||||
change.resource_name, named_route='resource.read', id=change.pkg_id,
|
|
||||||
resource_id=change.resource_id, qualified=True)
|
|
||||||
) }}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,17 +0,0 @@
|
||||||
{% set dataset_type = request.view_args.package_type or pkg_dict['type'] or 'dataset' %}
|
|
||||||
{% set pkg_url = h.url_for(dataset_type ~ '.read', id=change.pkg_id) %}
|
|
||||||
{% set resource_url = h.url_for(dataset_type ~ '_resource.read', id=change.pkg_id, resource_id = change.resource_id, qualified=True) %}
|
|
||||||
|
|
||||||
{% set pkg_link %}
|
|
||||||
<a href="{{ pkg_url }}">{{ change.title }}</a>
|
|
||||||
{% endset %}
|
|
||||||
|
|
||||||
{% set resource_link %}
|
|
||||||
<a href="{{ resource_url }}">{{ change.resource_name or _('Unnamed resource') }}</a>
|
|
||||||
{% endset %}
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{{ _('Added resource {resource_link} to {pkg_link}').format(pkg_link=pkg_link, resource_link=resource_link) }}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,5 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% if change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Updated description of {pkg_link} from <br class="line-height2"><blockquote>{old_notes}</blockquote> to <br class="line-height2"><blockquote>{new_notes}</blockquote>').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
old_notes = change.old_notes,
|
|
||||||
new_notes = change.new_notes
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "add" %}
|
|
||||||
|
|
||||||
{{ _('Updated description of {pkg_link} to <br class="line-height2"><blockquote>{new_notes}</blockquote>').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
new_notes = change.new_notes
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "remove" %}
|
|
||||||
|
|
||||||
{{ _('Removed description from {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id)
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% if change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Moved {pkg_link} from organization {old_org_link} to organization {new_org_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
old_org_link = h.nav_link(change.old_org_title, named_route='organization.read', id=change.old_org_id),
|
|
||||||
new_org_link = h.nav_link(change.new_org_title, named_route='organization.read', id=change.new_org_id)
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "remove" %}
|
|
||||||
|
|
||||||
{{ _('Removed {pkg_link} from organization {old_org_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
old_org_link = h.nav_link(change.old_org_title, named_route='organization.read', id=change.old_org_id)
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "add" %}
|
|
||||||
|
|
||||||
{{ _('Added {pkg_link} to organization {new_org_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
new_org_link = h.nav_link(change.new_org_title, named_route='organization.read', id=change.new_org_id)
|
|
||||||
) }}
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{{ _('Set visibility of {pkg_link} to {visibility}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
visibility = change.new
|
|
||||||
) }}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,39 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% if change.method == "add" %}
|
|
||||||
|
|
||||||
{{ _('Updated description of resource {resource_link} in {pkg_link} to <br class="line-height2"><blockquote>{new_desc}</blockquote>').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
resource_link = h.nav_link(
|
|
||||||
change.resource_name, named_route='resource.read', id=change.pkg_id,
|
|
||||||
resource_id=change.resource_id, qualified=True),
|
|
||||||
new_desc = change.new_desc
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "remove" %}
|
|
||||||
|
|
||||||
{{ _('Removed description from resource {resource_link} in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
resource_link = h.nav_link(
|
|
||||||
change.resource_name, named_route='resource.read', id=change.pkg_id,
|
|
||||||
resource_id=change.resource_id, qualified=True)
|
|
||||||
)}}
|
|
||||||
|
|
||||||
{% elif change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Updated description of resource {resource_link} in {pkg_link} from <br class="line-height2"><blockquote>{old_desc}</blockquote> to <br class="line-height2"><blockquote>{new_desc}</blockquote>').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
resource_link = h.nav_link(
|
|
||||||
change.resource_name, named_route='resource.read', id=change.pkg_id,
|
|
||||||
resource_id=change.resource_id, qualified=True),
|
|
||||||
new_desc = change.new_desc,
|
|
||||||
old_desc = change.old_desc
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,112 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% if change.method == "add_one_value" %}
|
|
||||||
|
|
||||||
{{ _('Added field <q>{key}</q> with value <q>{value}</q> to resource {resource_link} in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
resource_link = h.nav_link(
|
|
||||||
change.resource_name, named_route='resource.read', id=change.pkg_id,
|
|
||||||
resource_id=change.resource_id, qualified=True),
|
|
||||||
key = change.key,
|
|
||||||
value = change.value
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "add_one_no_value" %}
|
|
||||||
|
|
||||||
{{ _('Added field <q>{key}</q> to resource {resource_link} in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
resource_link = h.nav_link(
|
|
||||||
change.resource_name, named_route='resource.read', id=change.pkg_id,
|
|
||||||
resource_id=change.resource_id, qualified=True),
|
|
||||||
key = change.key,
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "add_multiple" %}
|
|
||||||
|
|
||||||
{{ _('Added the following fields to resource {resource_link} in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
resource_link = h.nav_link(
|
|
||||||
change.resource_name, named_route='resource.read', id=change.pkg_id,
|
|
||||||
resource_id=change.resource_id, qualified=True)
|
|
||||||
) }}
|
|
||||||
<ul>
|
|
||||||
{% for item in change.key_list %}
|
|
||||||
{% if change.value_list[item] != "" %}
|
|
||||||
{{ _('{key} with value {value}').format(
|
|
||||||
key = item,
|
|
||||||
value = change.value_list[item]
|
|
||||||
)|safe }}
|
|
||||||
{% else %}
|
|
||||||
{{ _('{key}').format(
|
|
||||||
key = item
|
|
||||||
)|safe }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{% elif change.method == "remove_one" %}
|
|
||||||
|
|
||||||
{{ _('Removed field <q>{key}</q> from resource {resource_link} in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
resource_link = h.nav_link(
|
|
||||||
change.resource_name, named_route='resource.read', id=change.pkg_id,
|
|
||||||
resource_id=change.resource_id, qualified=True),
|
|
||||||
key = change.key
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "remove_multiple" %}
|
|
||||||
|
|
||||||
{{ _('Removed the following fields from resource {resource_link} in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
resource_link = h.nav_link(
|
|
||||||
change.resource_name, named_route='resource.read', id=change.pkg_id,
|
|
||||||
resource_id=change.resource_id, qualified=True)
|
|
||||||
) }}
|
|
||||||
<ul>
|
|
||||||
{% for item in change.key_list %}
|
|
||||||
{{ _('{key}').format(
|
|
||||||
key = item
|
|
||||||
)|safe }}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{% elif change.method == "change_value_with_old" %}
|
|
||||||
|
|
||||||
{{ _('Changed value of field <q>{key}</q> of resource {resource_link} to <q>{new_val}</q> (previously <q>{old_val}</q>) in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
resource_link = h.nav_link(
|
|
||||||
change.resource_name, named_route='resource.read', id=change.pkg_id,
|
|
||||||
resource_id=change.resource_id, qualified=True),
|
|
||||||
key = change.key,
|
|
||||||
new_val = change.new_value,
|
|
||||||
old_val = change.old_value
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "change_value_no_old" %}
|
|
||||||
|
|
||||||
{{ _('Changed value of field <q>{key}</q> to <q>{new_val}</q> in resource {resource_link} in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
resource_link = h.nav_link(
|
|
||||||
change.resource_name, named_route='resource.read', id=change.pkg_id,
|
|
||||||
resource_id=change.resource_id, qualified=True),
|
|
||||||
key = change.key,
|
|
||||||
new_val = change.new_value
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "change_value_no_new" %}
|
|
||||||
|
|
||||||
{{ _('Removed the value of field <q>{key}</q> in resource {resource_link} in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
resource_link = h.nav_link(
|
|
||||||
change.resource_name, named_route='resource.read', id=change.pkg_id,
|
|
||||||
resource_id=change.resource_id, qualified=True),
|
|
||||||
key = change.key,
|
|
||||||
new_val = change.new_value
|
|
||||||
) }}
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,54 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% set format_search_base_url = (
|
|
||||||
h.url_for("organization.read", id=change.org_id)
|
|
||||||
if change.org_id else
|
|
||||||
h.url_for("dataset.search")) %}
|
|
||||||
{% set resource_url = (h.url_for(
|
|
||||||
"resource.read",
|
|
||||||
id=change.pkg_id,
|
|
||||||
resource_id=change.resource_id,
|
|
||||||
qualified=True)) %}
|
|
||||||
|
|
||||||
{% if change.method == "add" %}
|
|
||||||
|
|
||||||
{{ _('Set format of resource') }}
|
|
||||||
<a href="{{resource_url}}">
|
|
||||||
{{change.resource_name}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{format_search_base_url + '?res_format=' + change.format}}">
|
|
||||||
{{change.format}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('in') }}
|
|
||||||
<a href="{{ h.url_for('dataset.read', id=change.pkg_id) }}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% elif change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Set format of resource') }}
|
|
||||||
<a href="{{resource_url}}">
|
|
||||||
{{change.resource_name}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{format_search_base_url + '?res_format=' + change.new_format}}">
|
|
||||||
{{change.new_format}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('(previously') }}
|
|
||||||
<a href="{{format_search_base_url + '?res_format=' + change.old_format}}">
|
|
||||||
{{change.old_format}}
|
|
||||||
</a>
|
|
||||||
{{ _(')') }}
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{{ _('Renamed resource {old_resource_link} to {new_resource_link} in {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
old_resource_link = h.nav_link(
|
|
||||||
change.old_resource_name, named_route='resource.read', id=change.old_pkg_id,
|
|
||||||
resource_id=change.resource_id, activity_id=change.old_activity_id),
|
|
||||||
new_resource_link = h.nav_link(
|
|
||||||
change.new_resource_name, named_route='resource.read', id=change.new_pkg_id,
|
|
||||||
resource_id=change.resource_id),
|
|
||||||
) }}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,59 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% if change.method == "remove_one" %}
|
|
||||||
|
|
||||||
{{ _('Removed tag {tag_link} from {pkg_link}').format(
|
|
||||||
tag_link = '<a href="{tag_url}">{tag}</a>'.format(
|
|
||||||
tag_url = h.url_for('dataset.search') +
|
|
||||||
"?tags=" + change.tag,
|
|
||||||
tag = change.tag
|
|
||||||
)|safe,
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "remove_multiple" %}
|
|
||||||
|
|
||||||
{{ _('Removed the following tags from {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
)|safe }}
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{% for item in change.tags %}
|
|
||||||
<li>
|
|
||||||
{{ _('{tag_link}').format(
|
|
||||||
tag_link = h.nav_link(item, named_route='dataset.search', tags=item),
|
|
||||||
)}}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{% elif change.method == "add_one" %}
|
|
||||||
|
|
||||||
{{ _('Added tag {tag_link} to {pkg_link}').format(
|
|
||||||
tag_link = h.nav_link(change.tag, named_route='dataset.search', tags=change.tag),
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "add_multiple" %}
|
|
||||||
|
|
||||||
{{ _('Added the following tags to {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
) }}
|
|
||||||
<ul>
|
|
||||||
{% for item in change.tags %}
|
|
||||||
<li>
|
|
||||||
{# TODO: figure out which controller to actually use here #}
|
|
||||||
{{ _('{tag_link}').format(
|
|
||||||
tag_link = h.nav_link(item, named_route='dataset.search', tags=item),
|
|
||||||
)}}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{{ _('Changed title to {title_link} (previously {old_title})').format(
|
|
||||||
title_link = h.nav_link(change.new_title, named_route='dataset.read', id=change.id),
|
|
||||||
old_title = change.old_title
|
|
||||||
) }}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,46 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% set pkg_link = (h.url_for('dataset.read', id=change.pkg_id)) %}
|
|
||||||
{% if change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Changed the source URL of') }}
|
|
||||||
<a href="{{pkg_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('from') }}
|
|
||||||
<a href="{{'http://' + change.old_url if not change.old_url[0:4] == 'http' else change.old_url}}">
|
|
||||||
{{change.old_url}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{'http://' + change.new_url if not change.new_url[0:4] == 'http' else change.new_url}}">
|
|
||||||
{{change.new_url}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% elif change.method == "remove" %}
|
|
||||||
|
|
||||||
{{ _('Removed the source URL from') }}
|
|
||||||
<a href="{{pkg_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% elif change.method == "add" %}
|
|
||||||
|
|
||||||
{{_('Changed the source URL of') }}
|
|
||||||
<a href="{{pkg_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{'http://' + change.new_url if not change.new_url[0:4] == 'http' else change.new_url}}">
|
|
||||||
{{change.new_url}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% if change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Changed the version of {pkg_link} to {new_version} (previously {old_version})').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
old_version = change.old_version,
|
|
||||||
new_version = change.new_version
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "remove" %}
|
|
||||||
|
|
||||||
{{ _('Removed the version from {pkg_link}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "add" %}
|
|
||||||
|
|
||||||
{{ _('Changed the version of {pkg_link} to {new_version}').format(
|
|
||||||
pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id),
|
|
||||||
new_version = change.new_version
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% if change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Updated description of {group_link} from <br class="line-height2"><blockquote>{old_description}</blockquote> to <br class="line-height2"><blockquote>{new_description}</blockquote>').format(
|
|
||||||
group_link = h.nav_link(change.title, named_route='group.read', id=change.pkg_id),
|
|
||||||
old_description = change.old_description,
|
|
||||||
new_description = change.new_description
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "add" %}
|
|
||||||
|
|
||||||
{{ _('Updated description of {group_link} to <br class="line-height2"><blockquote>{new_description}</blockquote>').format(
|
|
||||||
group_link = h.nav_link(change.title, named_route='group.read', id=change.pkg_id),
|
|
||||||
new_description = change.new_description
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "remove" %}
|
|
||||||
|
|
||||||
{{ _('Removed description from {group_link}').format(
|
|
||||||
group_link = h.nav_link(change.title, named_route='group.read', id=change.pkg_id)
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,46 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% set group_link = (h.url_for('group.read', id=change.pkg_id)) %}
|
|
||||||
{% if change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Changed the image URL of') }}
|
|
||||||
<a href="{{group_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('from') }}
|
|
||||||
<a href="{{'http://' + change.old_image_url if not change.old_image_url[0:4] == 'http' else change.old_image_url}}">
|
|
||||||
{{change.old_image_url}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{'http://' + change.new_image_url if not change.new_image_url[0:4] == 'http' else change.new_image_url}}">
|
|
||||||
{{change.new_image_url}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% elif change.method == "remove" %}
|
|
||||||
|
|
||||||
{{ _('Removed the image URL from') }}
|
|
||||||
<a href="{{group_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% elif change.method == "add" %}
|
|
||||||
|
|
||||||
{{ _('Changed the image URL of') }}
|
|
||||||
<a href="{{group_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{'http://' + change.new_image_url if not change.new_image_url[0:4] == 'http' else change.new_image_url}}">
|
|
||||||
{{change.new_image_url}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,5 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{{ _('Changed title to {title_link} (previously {old_title})').format(
|
|
||||||
title_link = h.nav_link(change.new_title, named_route='group.read', id=change.id),
|
|
||||||
old_title = change.old_title
|
|
||||||
) }}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% if change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Updated description of {org_link} from <br class="line-height2"><blockquote>{old_description}</blockquote> to <br class="line-height2"><blockquote>{new_description}</blockquote>').format(
|
|
||||||
org_link = h.nav_link(change.title, named_route='organization.read', id=change.pkg_id),
|
|
||||||
old_description = change.old_description,
|
|
||||||
new_description = change.new_description
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "add" %}
|
|
||||||
|
|
||||||
{{ _('Updated description of {org_link} to <br class="line-height2"><blockquote>{new_description}</blockquote>').format(
|
|
||||||
org_link = h.nav_link(change.title, named_route='organization.read', id=change.pkg_id),
|
|
||||||
new_description = change.new_description
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% elif change.method == "remove" %}
|
|
||||||
|
|
||||||
{{ _('Removed description from {org_link}').format(
|
|
||||||
org_link = h.nav_link(change.title, named_route='organization.read', id=change.pkg_id)
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,46 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% set org_link = (h.url_for('organization.read', id=change.pkg_id)) %}
|
|
||||||
{% if change.method == "change" %}
|
|
||||||
|
|
||||||
{{ _('Changed the image URL of') }}
|
|
||||||
<a href="{{org_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('from') }}
|
|
||||||
<a href="{{'http://' + change.old_image_url if not change.old_image_url[0:4] == 'http' else change.old_image_url}}">
|
|
||||||
{{change.old_image_url}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{'http://' + change.new_image_url if not change.new_image_url[0:4] == 'http' else change.new_image_url}}">
|
|
||||||
{{change.new_image_url}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% elif change.method == "remove" %}
|
|
||||||
|
|
||||||
{{ _('Removed the image URL from') }}
|
|
||||||
<a href="{{org_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% elif change.method == "add" %}
|
|
||||||
|
|
||||||
{{ _('Changed the image URL of') }}
|
|
||||||
<a href="{{org_link}}">
|
|
||||||
{{change.title}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ _('to') }}
|
|
||||||
<a href="{{'http://' + change.new_image_url if not change.new_image_url[0:4] == 'http' else change.new_image_url}}">
|
|
||||||
{{change.new_image_url}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,5 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{{ _('Changed title to {title_link} (previously {old_title})').format(
|
|
||||||
title_link = h.nav_link(change.new_title, named_route='organization.read', id=change.id),
|
|
||||||
old_title = change.old_title
|
|
||||||
) }}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
|
@ -1,7 +0,0 @@
|
||||||
{% set class_prev = "btn btn-default" if newer_activities_url else "btn disabled" %}
|
|
||||||
{% set class_next = "btn btn-default" if older_activities_url else "btn disabled" %}
|
|
||||||
|
|
||||||
<div id="activity_page_buttons" class="activity_buttons" style="margin-top: 25px;">
|
|
||||||
<a href="{{ newer_activities_url }}" class="{{ class_prev }}">{{ _('Newer activities') }}</a>
|
|
||||||
<a href="{{ older_activities_url }}" class="{{ class_next }}">{{ _('Older activities') }}</a>
|
|
||||||
</div>
|
|
|
@ -1,63 +0,0 @@
|
||||||
{% macro actor(activity) %}
|
|
||||||
<span>
|
|
||||||
{{ h.linked_user(activity.user_id, 0, 30) }}
|
|
||||||
</span>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro dataset(activity) %}
|
|
||||||
{% set dataset_type = activity.data.package.type or 'dataset' %}
|
|
||||||
<span class="dataset">
|
|
||||||
<a href="{{ h.url_for(dataset_type ~ '.read', id=activity.object_id ) }}">
|
|
||||||
{{ activity.data.package.title if activity.data.package else _("unknown") }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro organization(activity) %}
|
|
||||||
{% set group_type = group_type or (activity.data.group.type if (activity.data.group and activity.data.group.type) else 'organization') %}
|
|
||||||
<a href="{{ h.url_for(group_type ~ '.read', id=activity.object_id) }}">
|
|
||||||
{{ activity.data.group.title if activity.data.group else _('unknown') }}
|
|
||||||
</a>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro user(activity) %}
|
|
||||||
<span>
|
|
||||||
{{ h.linked_user(activity.object_id, 0, 20) }}
|
|
||||||
</span>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro group(activity) %}
|
|
||||||
<span class="group">
|
|
||||||
<a href="{{ h.url_for('group.read', id=activity.object_id) }}">
|
|
||||||
{{ activity.data.group.title if activity.data.group else _('unknown') }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{# Renders the actual stream of activities
|
|
||||||
|
|
||||||
activity_stream - the activity data. e.g. the output from package_activity_list
|
|
||||||
id - the id or current name of the object (e.g. package name, user id)
|
|
||||||
object_type - 'package', 'organization', 'group', 'user'
|
|
||||||
|
|
||||||
#}
|
|
||||||
{% block activity_stream %}
|
|
||||||
<ul class="activity">
|
|
||||||
{% set can_show_activity_detail = h.check_access('activity_list', {'id': id, 'include_data': True, 'object_type': object_type}) %}
|
|
||||||
|
|
||||||
{% for activity in activity_stream %}
|
|
||||||
{%- snippet "snippets/activities/{}.html".format(activity.activity_type.replace(' ', '_')),
|
|
||||||
"snippets/activities/fallback.html",
|
|
||||||
activity=activity, can_show_activity_detail=can_show_activity_detail,
|
|
||||||
ah={
|
|
||||||
'actor': actor,
|
|
||||||
'dataset': dataset,
|
|
||||||
'organization': organization,
|
|
||||||
'user': user,
|
|
||||||
'group': group,
|
|
||||||
},
|
|
||||||
id=id
|
|
||||||
-%}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
|
@ -1,14 +0,0 @@
|
||||||
{% extends "user/read.html" %}
|
|
||||||
|
|
||||||
{% block subtitle %}{{ _('Activity Stream') }} {{ g.template_title_delimiter }} {{ super() }}{% endblock %}
|
|
||||||
|
|
||||||
{% block primary_content_inner %}
|
|
||||||
<h1 class="hide-heading">
|
|
||||||
{% block page_heading -%}
|
|
||||||
{{ _('Activity Stream') }}
|
|
||||||
{%- endblock %}
|
|
||||||
</h1>
|
|
||||||
{% snippet 'snippets/stream.html', activity_stream=activity_stream, id=id, object_type='user' %}
|
|
||||||
|
|
||||||
{% snippet 'snippets/pagination.html', newer_activities_url=newer_activities_url, older_activities_url=older_activities_url %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,27 +0,0 @@
|
||||||
{% ckan_extends %}
|
|
||||||
|
|
||||||
{% block breadcrumb_content %}
|
|
||||||
<li class="active"><a href="{{ h.url_for('activity.dashboard') }}">{{ _('Dashboard') }}</a></li>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block dashboard_nav_links %}
|
|
||||||
{{ h.build_nav_icon('activity.dashboard', _('News feed'), icon='list') }}
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block primary_content_inner %}
|
|
||||||
<div data-module="dashboard">
|
|
||||||
{% snippet 'user/snippets/followee_dropdown.html', context=dashboard_activity_stream_context, followees=followee_list %}
|
|
||||||
<h2 class="page-heading mb-4">
|
|
||||||
{% block page_heading %}
|
|
||||||
{{ _('News feed') }}
|
|
||||||
{% endblock %}
|
|
||||||
<small class="text-muted fs-5">{{ _("Activity from items that I'm following") }}</small>
|
|
||||||
</h2>
|
|
||||||
{% snippet 'snippets/stream.html', activity_stream=dashboard_activity_stream %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% snippet 'snippets/pagination.html', newer_activities_url=newer_activities_url, older_activities_url=older_activities_url %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,12 +0,0 @@
|
||||||
{% ckan_extends %}
|
|
||||||
|
|
||||||
{% block extra_fields %}
|
|
||||||
{% if h.activity_show_email_notifications() %}
|
|
||||||
{% call form.checkbox('activity_streams_email_notifications', label=_('Subscribe to notification emails'), id='field-activity-streams-email-notifications', value=True, checked=g.userobj.activity_streams_email_notifications) %}
|
|
||||||
{% set helper_text = _("You will receive notification emails from {site_title}, e.g. when you have new activities on your dashboard."|string) %}
|
|
||||||
{{ form.info(helper_text.format(site_title=g.site_title)) }}
|
|
||||||
{% endcall %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue