Skip to content

Commit b1273ef

Browse files
committed
feat: share dialog for collections and datasets
1 parent 51a271a commit b1273ef

4 files changed

Lines changed: 421 additions & 49 deletions

File tree

CHANGELOG

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
0.18.3
1+
0.19.0
2+
- feat: share dialog for collections and datasets
23
- fix: remember ETag of uploaded resources (#80)
4+
- fix: create a new group if when no group available to add datasets to
35
- enh: allow sharing individual datasets instead of collections (#32)
46
- enh: add preview option for dataset description in upload dialog (#52)
57
- enh: introduce write lock for eternal job updates
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
from functools import partial
2+
from importlib import resources
3+
import json
4+
import logging
5+
import pathlib
6+
from typing import Literal
7+
8+
from PyQt6 import uic, QtCore, QtGui, QtWidgets
9+
10+
from ...api import errors
11+
from ..api import get_ckan_api
12+
13+
14+
class ShareDialog(QtWidgets.QDialog):
15+
def __init__(self,
16+
parent,
17+
which: Literal["collection", "dataset"],
18+
identifier: str,
19+
*args, **kwargs):
20+
"""Create a new window for setting up a file upload
21+
"""
22+
super(ShareDialog, self).__init__(parent, *args, **kwargs)
23+
self.logger = logging.getLogger(__name__)
24+
ref_ui = resources.files(
25+
"dcoraid.gui.panel_my_data") / "dlg_share.ui"
26+
with resources.as_file(ref_ui) as path_ui:
27+
uic.loadUi(path_ui, self)
28+
29+
self.api = get_ckan_api()
30+
31+
self.which = which
32+
self.identifier = identifier
33+
34+
self.setWindowTitle(f"Share {self.which}")
35+
self.label.setText(f"Share {self.which} '{self.identifier}'")
36+
37+
self.toolButton_share.clicked.connect(self.on_user_add)
38+
self.comboBox_users.currentIndexChanged.connect(self.on_combobox_users)
39+
self.comboBox_users.currentTextChanged.connect(self.on_search)
40+
41+
self.toolButton_share.setEnabled(False)
42+
43+
self.tableWidget_users.user_remove.connect(self.on_user_remove)
44+
45+
# Determine users that currently have access
46+
try:
47+
if self.which == "collection":
48+
group = self.api.get("group_show",
49+
id=self.identifier,
50+
include_users=True
51+
)
52+
users = group["users"]
53+
else:
54+
users = self.api.get("package_collaborator_list",
55+
id=self.identifier)
56+
except errors.APIAuthorizationError:
57+
QtWidgets.QMessageBox.critical(
58+
self,
59+
"Insufficient permissions",
60+
f"You do not have sufficient authorization to modify "
61+
f"the {self.which} '{self.identifier}'.")
62+
self.close()
63+
else:
64+
self.tableWidget_users.users += users
65+
self.tableWidget_users.update_user_table()
66+
67+
@QtCore.pyqtSlot()
68+
def on_user_add(self):
69+
user = self.comboBox_users.currentData()
70+
if self.which == "dataset":
71+
self.api.post("package_collaborator_create",
72+
data={"id": self.identifier,
73+
"user_id": user["id"],
74+
"capacity": "member"})
75+
else:
76+
self.api.post("group_member_create",
77+
data={"id": self.identifier,
78+
"username": user["id"],
79+
"role": "member"})
80+
self.tableWidget_users.on_user_add(user)
81+
82+
@QtCore.pyqtSlot(dict)
83+
def on_user_remove(self, user_dict):
84+
"""Remove a user from the given collection or dataset"""
85+
if self.which == "dataset":
86+
self.api.post("package_collaborator_delete",
87+
data={"id": self.identifier,
88+
"user_id": user_dict["id"]})
89+
else:
90+
self.api.post("group_member_delete",
91+
data={"id": self.identifier,
92+
"username": user_dict["id"]})
93+
94+
@QtCore.pyqtSlot()
95+
def on_combobox_users(self):
96+
index = self.comboBox_users.currentIndex()
97+
self.toolButton_share.setEnabled(index >= 0)
98+
99+
@QtCore.pyqtSlot()
100+
def on_search(self):
101+
"""Search for users given the current string and set combobox items"""
102+
search_text = self.comboBox_users.currentText()
103+
if len(search_text) <= 3:
104+
self.toolButton_share.setEnabled(False)
105+
return
106+
107+
users = self.api.get("user_autocomplete",
108+
q=search_text,
109+
limit=100)
110+
# Populate the combobox
111+
if users:
112+
cur_users = []
113+
for ii in range(self.comboBox_users.count()):
114+
cur_users.append(self.comboBox_users.itemData(ii)["id"])
115+
if set(cur_users) != set([u["id"] for u in users]):
116+
# Update users listed in combobox
117+
self.comboBox_users.clear()
118+
for ii, user in enumerate(users):
119+
user = self.tableWidget_users.get_full_user_dict(user)
120+
self.comboBox_users.blockSignals(True)
121+
self.comboBox_users.addItem(user["displayname"], user)
122+
self.comboBox_users.blockSignals(False)
123+
124+
125+
class UserTableWidget(QtWidgets.QTableWidget):
126+
user_remove = QtCore.pyqtSignal(dict)
127+
128+
def __init__(self, *args, **kwargs):
129+
super(UserTableWidget, self).__init__(*args, **kwargs)
130+
131+
self.api = get_ckan_api()
132+
self.user_cache_location = pathlib.Path(
133+
QtCore.QStandardPaths.writableLocation(
134+
QtCore.QStandardPaths.StandardLocation.CacheLocation)
135+
) / self.api.hostname / "user_dicts"
136+
self.user_cache_location.mkdir(parents=True, exist_ok=True)
137+
138+
# Set column count and horizontal header sizes
139+
self.setColumnCount(2)
140+
header = self.horizontalHeader()
141+
header.setSectionResizeMode(
142+
0, QtWidgets.QHeaderView.ResizeMode.Stretch)
143+
header.setSectionResizeMode(
144+
1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
145+
146+
# Add current users
147+
self.users = []
148+
self.update_user_table()
149+
150+
def get_full_user_dict(self, user_dict):
151+
"""Cache and/or return a user dictionary"""
152+
uid = user_dict.get("id", user_dict.get("user_id"))
153+
user_cache = self.user_cache_location / f"{uid}.json"
154+
if user_cache.exists():
155+
cur_dict = json.loads(user_cache.read_text())
156+
else:
157+
cur_dict = {"id": uid}
158+
if "name" in user_dict:
159+
cur_dict["name"] = user_dict["name"]
160+
if "fullname" in user_dict:
161+
cur_dict["fullname"] = user_dict["fullname"]
162+
if cur_dict.get("fullname") and cur_dict.get("name"):
163+
dname = f"{cur_dict['fullname']} ({cur_dict['name']})"
164+
else:
165+
dname = cur_dict.get('name', cur_dict.get("id", "unknown")[:7])
166+
cur_dict["displayname"] = dname
167+
user_cache.write_text(json.dumps(cur_dict))
168+
return cur_dict
169+
170+
def update_user_table(self):
171+
"""Add user to table widget"""
172+
self.setRowCount(len(self.users))
173+
for row, user in enumerate(self.users):
174+
user = self.get_full_user_dict(user)
175+
self.set_label_item(row, 0, user["displayname"])
176+
if user["id"] != self.api.user_id:
177+
self.set_actions_item(row, 1, user)
178+
QtWidgets.QApplication.processEvents(
179+
QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 300)
180+
181+
@QtCore.pyqtSlot(dict)
182+
def on_user_add(self, user_dict):
183+
self.users.append(self.get_full_user_dict(user_dict))
184+
self.update_user_table()
185+
186+
@QtCore.pyqtSlot(str)
187+
def on_user_remove(self, user_dict):
188+
self.user_remove.emit(user_dict)
189+
ids = [self.get_full_user_dict(u)["id"] for u in self.users]
190+
idx = ids.index(user_dict["id"])
191+
self.users.pop(idx)
192+
self.update_user_table()
193+
194+
def set_label_item(self, row, col, label):
195+
"""Get/Create a Qlabel at the specified position
196+
197+
User has to make sure that row and column count are set
198+
"""
199+
label = f"{label}"
200+
item = self.item(row, col)
201+
if item is None:
202+
item = QtWidgets.QTableWidgetItem(label)
203+
item.setToolTip(label)
204+
item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled)
205+
self.setItem(row, col, item)
206+
else:
207+
if item.text() != label:
208+
item.setText(label)
209+
item.setToolTip(label)
210+
211+
def set_actions_item(self, row, col, user_dict):
212+
"""Set/Create a TableCellActions widget in the table
213+
214+
Refreshes the widget and also connects signals.
215+
"""
216+
widact = self.cellWidget(row, col)
217+
if widact is None:
218+
widact = QtWidgets.QWidget(self)
219+
horz_layout = QtWidgets.QHBoxLayout(widact)
220+
horz_layout.setContentsMargins(2, 0, 2, 0)
221+
222+
actions = [
223+
{"icon": "trash",
224+
"tooltip": f"Remove user {user_dict['displayname']}",
225+
"function": partial(self.on_user_remove, user_dict)
226+
}
227+
]
228+
for action in actions:
229+
tbact = QtWidgets.QToolButton(widact)
230+
icon = QtGui.QIcon.fromTheme(action["icon"])
231+
tbact.setIcon(icon)
232+
tbact.setToolTip(action["tooltip"])
233+
tbact.clicked.connect(action["function"])
234+
horz_layout.addWidget(tbact)
235+
self.setCellWidget(row, col, widact)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>Dialog</class>
4+
<widget class="QDialog" name="Dialog">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>400</width>
10+
<height>300</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>Dialog</string>
15+
</property>
16+
<layout class="QVBoxLayout" name="verticalLayout">
17+
<item>
18+
<widget class="QLabel" name="label">
19+
<property name="text">
20+
<string>Share collection 'name and identifier'</string>
21+
</property>
22+
</widget>
23+
</item>
24+
<item>
25+
<layout class="QHBoxLayout" name="horizontalLayout">
26+
<item>
27+
<widget class="QComboBox" name="comboBox_users">
28+
<property name="editable">
29+
<bool>true</bool>
30+
</property>
31+
<property name="placeholderText">
32+
<string>Type a username</string>
33+
</property>
34+
</widget>
35+
</item>
36+
<item>
37+
<widget class="QToolButton" name="toolButton_share">
38+
<property name="text">
39+
<string>Share with User</string>
40+
</property>
41+
</widget>
42+
</item>
43+
</layout>
44+
</item>
45+
<item>
46+
<widget class="UserTableWidget" name="tableWidget_users">
47+
<property name="alternatingRowColors">
48+
<bool>true</bool>
49+
</property>
50+
<property name="selectionMode">
51+
<enum>QAbstractItemView::SelectionMode::NoSelection</enum>
52+
</property>
53+
<property name="cornerButtonEnabled">
54+
<bool>false</bool>
55+
</property>
56+
<property name="columnCount">
57+
<number>2</number>
58+
</property>
59+
<attribute name="horizontalHeaderVisible">
60+
<bool>false</bool>
61+
</attribute>
62+
<attribute name="horizontalHeaderMinimumSectionSize">
63+
<number>50</number>
64+
</attribute>
65+
<column>
66+
<property name="text">
67+
<string>User</string>
68+
</property>
69+
</column>
70+
<column>
71+
<property name="text">
72+
<string>Actions</string>
73+
</property>
74+
</column>
75+
</widget>
76+
</item>
77+
<item>
78+
<widget class="QDialogButtonBox" name="buttonBox">
79+
<property name="orientation">
80+
<enum>Qt::Orientation::Horizontal</enum>
81+
</property>
82+
<property name="standardButtons">
83+
<set>QDialogButtonBox::StandardButton::Close</set>
84+
</property>
85+
</widget>
86+
</item>
87+
</layout>
88+
</widget>
89+
<customwidgets>
90+
<customwidget>
91+
<class>UserTableWidget</class>
92+
<extends>QTableWidget</extends>
93+
<header>dcoraid.gui.panel_my_data.dlg_share</header>
94+
</customwidget>
95+
</customwidgets>
96+
<resources/>
97+
<connections>
98+
<connection>
99+
<sender>buttonBox</sender>
100+
<signal>accepted()</signal>
101+
<receiver>Dialog</receiver>
102+
<slot>accept()</slot>
103+
<hints>
104+
<hint type="sourcelabel">
105+
<x>248</x>
106+
<y>254</y>
107+
</hint>
108+
<hint type="destinationlabel">
109+
<x>157</x>
110+
<y>274</y>
111+
</hint>
112+
</hints>
113+
</connection>
114+
<connection>
115+
<sender>buttonBox</sender>
116+
<signal>rejected()</signal>
117+
<receiver>Dialog</receiver>
118+
<slot>reject()</slot>
119+
<hints>
120+
<hint type="sourcelabel">
121+
<x>316</x>
122+
<y>260</y>
123+
</hint>
124+
<hint type="destinationlabel">
125+
<x>286</x>
126+
<y>274</y>
127+
</hint>
128+
</hints>
129+
</connection>
130+
</connections>
131+
</ui>

0 commit comments

Comments
 (0)