importo template da ckan 2.10
This commit is contained in:
parent
fe475f6ce5
commit
94c75b28bf
2
.env
2
.env
|
@ -78,7 +78,7 @@ NGINX_PORT=80
|
||||||
NGINX_SSLPORT=443
|
NGINX_SSLPORT=443
|
||||||
|
|
||||||
# Extensions
|
# Extensions
|
||||||
CKAN__PLUGINS="envvars image_view text_view recline_view datastore datapusher d4science_theme dcat dcat_rdf_harvester dcat_json_harvester dcat_json_interface structured_data harvest ckan_harvester spatial_metadata spatial_query"
|
CKAN__PLUGINS="envvars image_view text_view recline_view datastore datapusher d4science_theme activity dcat dcat_rdf_harvester dcat_json_harvester dcat_json_interface structured_data harvest ckan_harvester spatial_metadata spatial_query"
|
||||||
CKAN__HARVEST__MQ__TYPE=redis
|
CKAN__HARVEST__MQ__TYPE=redis
|
||||||
CKAN__HARVEST__MQ__HOSTNAME=redis
|
CKAN__HARVEST__MQ__HOSTNAME=redis
|
||||||
CKAN__HARVEST__MQ__PORT=6379
|
CKAN__HARVEST__MQ__PORT=6379
|
||||||
|
|
|
@ -7,3 +7,4 @@ _service-provider/*
|
||||||
_solr/schema.xml
|
_solr/schema.xml
|
||||||
_src/*
|
_src/*
|
||||||
local/*
|
local/*
|
||||||
|
.vscode/settings.json
|
||||||
|
|
|
@ -13,12 +13,11 @@ import ckan.plugins.toolkit as toolkit
|
||||||
class D4SciencePlugin(plugins.SingletonPlugin):
|
class D4SciencePlugin(plugins.SingletonPlugin):
|
||||||
plugins.implements(plugins.IConfigurer)
|
plugins.implements(plugins.IConfigurer)
|
||||||
|
|
||||||
# plugins.implements(plugins.IAuthFunctions)
|
plugins.implements(plugins.IAuthFunctions)
|
||||||
# plugins.implements(plugins.IActions)
|
plugins.implements(plugins.IActions)
|
||||||
# plugins.implements(plugins.IBlueprint)
|
|
||||||
# plugins.implements(plugins.IClick)
|
# plugins.implements(plugins.IClick)
|
||||||
# plugins.implements(plugins.ITemplateHelpers)
|
plugins.implements(plugins.ITemplateHelpers)
|
||||||
# plugins.implements(plugins.IValidators)
|
plugins.implements(plugins.IValidators)
|
||||||
|
|
||||||
#ckan 2.10
|
#ckan 2.10
|
||||||
plugins.implements(plugins.IBlueprint)
|
plugins.implements(plugins.IBlueprint)
|
||||||
|
@ -50,7 +49,6 @@ class D4SciencePlugin(plugins.SingletonPlugin):
|
||||||
def get_blueprint(self):
|
def get_blueprint(self):
|
||||||
blueprint = Blueprint('foo', self.__module__)
|
blueprint = Blueprint('foo', self.__module__)
|
||||||
rules = [
|
rules = [
|
||||||
('/foo', 'custom_action', custom_action),
|
|
||||||
('/group', 'group_index', custom_group_index),
|
('/group', 'group_index', custom_group_index),
|
||||||
]
|
]
|
||||||
for rule in rules:
|
for rule in rules:
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1,34 @@
|
||||||
|
/* 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;
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
});
|
|
@ -0,0 +1,103 @@
|
||||||
|
.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 */
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
.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;
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
/* 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
|
@ -0,0 +1,64 @@
|
||||||
|
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');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,14 @@
|
||||||
|
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
|
@ -0,0 +1,278 @@
|
||||||
|
# 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)
|
|
@ -0,0 +1,190 @@
|
||||||
|
# -*- 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")
|
|
@ -0,0 +1,647 @@
|
||||||
|
# -*- 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,
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
# -*- 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)
|
|
@ -0,0 +1,102 @@
|
||||||
|
# -*- 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
|
|
@ -0,0 +1,97 @@
|
||||||
|
# -*- 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
|
||||||
|
)
|
|
@ -0,0 +1,28 @@
|
||||||
|
# -*- 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
|
|
@ -0,0 +1,791 @@
|
||||||
|
# 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)
|
|
@ -0,0 +1,30 @@
|
||||||
|
# -*- 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.
After Width: | Height: | Size: 74 B |
|
@ -0,0 +1,177 @@
|
||||||
|
# -*- 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)
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{# 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>
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% ckan_extends %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{ super() }}
|
||||||
|
{% asset 'ckanext-activity/activity-css' %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,64 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,64 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,64 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,23 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,26 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% 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 %}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% ckan_extends %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
{% asset "ckanext-activity/activity" %}
|
||||||
|
{% endblock scripts %}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,26 @@
|
||||||
|
{% 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>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,17 @@
|
||||||
|
{% 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>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,38 @@
|
||||||
|
{#
|
||||||
|
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>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,22 @@
|
||||||
|
{% 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>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,25 @@
|
||||||
|
{#
|
||||||
|
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>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,85 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,67 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,17 @@
|
||||||
|
{% 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>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<li>
|
||||||
|
<p>
|
||||||
|
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
||||||
|
</p>
|
||||||
|
</li>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,112 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,59 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,46 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,46 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<li>
|
||||||
|
<p>
|
||||||
|
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
||||||
|
</p>
|
||||||
|
</li>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,46 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<li>
|
||||||
|
<p>
|
||||||
|
{{ _('No fields were updated. See the metadata diff for more details.') }}
|
||||||
|
</p>
|
||||||
|
</li>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% 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>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue