Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions ckanext/datagov_inventory/action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import ckan.model as model
import ckan.plugins.toolkit as toolkit


@toolkit.side_effect_free
def user_org_roles(context, data_dict):
"""Return users with organization roles, grouped by catagories."""
toolkit.check_access('user_org_roles', context, data_dict)

org_roles_by_user = _org_roles_by_user()
users = model.Session.query(model.User).filter(
model.User.state.in_([model.State.ACTIVE, model.State.DELETED])
).all()

result = []
for user in users:
organizations = sorted(
org_roles_by_user.get(user.id, []),
key=lambda org: (org['name'] or '').lower()
)
result.append({
'id': user.id,
'name': user.name,
'fullname': user.fullname,
'email': user.email,
'last_active': _format_datetime(user.last_active),
'state': user.state,
'sysadmin': user.sysadmin,
'organizations': organizations,
})

return sorted(result, key=_user_sort_key)


def _format_datetime(value):
if not value:
return ''
return value.replace(microsecond=0).isoformat(sep=' ')


def _org_roles_by_user():
"""
Retrieve a dictionary mapping user IDs to their roles within organizations.
"""
org_roles_by_user = {}
query = model.Session.query(model.Member, model.Group).join(
model.Group,
model.Member.group_id == model.Group.id
).filter(
model.Member.table_name == 'user',
model.Member.state == 'active',
model.Group.state == 'active',
model.Group.is_organization.is_(True),
)

for member, organization in query:
org_roles_by_user.setdefault(member.table_id, []).append({
'id': organization.id,
'name': organization.name,
'title': organization.title,
'role': member.capacity,
})

return org_roles_by_user


def _user_sort_key(user):
"""Sort users by display category, then by username within each category.

The category order is:
1. active sysadmins
2. active users with at least one organization role
3. active users without organization roles
4. deleted users
"""
if user['state'] == model.State.DELETED:
group = 3
elif user['sysadmin']:
group = 0
elif user['organizations']:
group = 1
else:
group = 2

return (group, (user['name'] or '').lower())
55 changes: 55 additions & 0 deletions ckanext/datagov_inventory/fanstatic/scripts/datagov_inventory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
(function () {
function cellText(row, index) {
return row.children[index].textContent.trim().toLowerCase();
}

function updateHeaders(buttons, sortState) {
buttons.forEach(function (button) {
var th = button.closest('th');
var indicator = '';
if (Number(th.dataset.sortIndex) === sortState.index) {
indicator = sortState.direction === 'asc' ? ' \u25B2' : ' \u25BC';
}
button.textContent = th.dataset.label + indicator;
});
}

function sortTable(table, buttons, sortState, index) {
if (sortState.index === index) {
sortState.direction = sortState.direction === 'asc' ? 'desc' : 'asc';
} else {
sortState.index = index;
sortState.direction = 'asc';
}

var tbody = table.querySelector('tbody');
var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
rows.sort(function (left, right) {
var leftText = cellText(left, index);
var rightText = cellText(right, index);
var comparison = leftText.localeCompare(rightText);
return sortState.direction === 'asc' ? comparison : -comparison;
});
rows.forEach(function (row) {
tbody.appendChild(row);
});
updateHeaders(buttons, sortState);
}

document.querySelectorAll('table[data-sortable-table]').forEach(function (table) {
var sortState = { index: 0, direction: 'asc' };
var buttons = table.querySelectorAll('th[data-sort-index] button');

buttons.forEach(function (button) {
button.addEventListener('click', function () {
sortTable(
table,
buttons,
sortState,
Number(button.closest('th').dataset.sortIndex)
);
});
});
updateHeaders(buttons, sortState);
});
}());
89 changes: 88 additions & 1 deletion ckanext/datagov_inventory/fanstatic/styles/datagov_inventory.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,91 @@
font-weight: 700;
float: right;
padding-right: 5px;
}
}

.user-org-roles .page-heading {
margin-bottom: 16px;
}

.user-org-roles-summary {
border-bottom: 1px solid #ddd;
margin-bottom: 24px;
padding-bottom: 16px;
}

.user-org-roles-summary ul {
display: flex;
flex-wrap: wrap;
gap: 8px;
list-style: none;
margin: 0;
padding: 0;
}

.user-org-roles-summary a {
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
display: inline-block;
padding: 6px 10px;
}

.user-org-roles-summary a:hover,
.user-org-roles-summary a:focus {
background-color: #fff;
border-color: #005ea8;
text-decoration: none;
}

.user-org-roles-summary span,
.user-org-roles-section h2 span {
color: #555;
font-size: 14px;
font-weight: 400;
margin-left: 4px;
}

.user-org-roles-section + .user-org-roles-section {
margin-top: 40px;
}

.user-org-roles-section h2 {
border-bottom: 1px solid #ddd;
font-size: 20px;
margin-bottom: 16px;
padding-bottom: 8px;
}

.user-org-roles .table {
margin-bottom: 0;
}

.user-org-roles-back-to-top {
margin: 10px 0 0;
text-align: right;
}

.user-org-roles-back-to-top a {
font-size: 14px;
}

.user-org-roles th.sortable {
background-color: #f5f5f5;
}

.user-org-roles-sort {
background: transparent;
border: 0;
color: #005ea8;
cursor: pointer;
font: inherit;
font-weight: 700;
padding: 0;
text-align: left;
}

.user-org-roles-sort:hover,
.user-org-roles-sort:focus {
color: #1a4480;
text-decoration: underline;
}
5 changes: 5 additions & 0 deletions ckanext/datagov_inventory/fanstatic/webassets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ styles:
output: datagov_inventory/datagov_inventory.css
contents: # list of files that are included into asset
- styles/datagov_inventory.css

scripts:
output: datagov_inventory/datagov_inventory.js
contents:
- scripts/datagov_inventory.js
Loading