diff --git a/src/calibre/gui2/preferences/server.py b/src/calibre/gui2/preferences/server.py
index 0ddb80e3b5..fb0c647029 100644
--- a/src/calibre/gui2/preferences/server.py
+++ b/src/calibre/gui2/preferences/server.py
@@ -7,16 +7,18 @@ import time
from PyQt5.Qt import (
QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout,
- QHBoxLayout, QLabel, QLineEdit, QPlainTextEdit, QPushButton, QScrollArea, QSize,
- QSizePolicy, QSpinBox, Qt, QTabWidget, QTimer, QUrl, QVBoxLayout, QWidget,
- pyqtSignal
+ QHBoxLayout, QIcon, QLabel, QLineEdit, QListWidget, QPlainTextEdit, QPushButton,
+ QScrollArea, QSize, QSizePolicy, QSpinBox, Qt, QTabWidget, QTimer, QUrl,
+ QVBoxLayout, QWidget, pyqtSignal
)
from calibre import as_unicode
from calibre.gui2 import config, error_dialog, info_dialog, open_url, warning_dialog
from calibre.gui2.preferences import AbortCommit, ConfigWidgetBase, test_widget
from calibre.srv.opts import change_settings, options, server_config
-from calibre.srv.users import UserManager
+from calibre.srv.users import UserManager, validate_username, validate_password, create_user_data
+from calibre.utils.icu import primary_sort_key
+
# Advanced {{{
@@ -273,16 +275,170 @@ class MainTab(QWidget): # {{{
# Users {{{
+
+class NewUser(QDialog):
+
+ def __init__(self, user_data, parent=None, username=None):
+ QDialog.__init__(self, parent)
+ self.user_data = user_data
+ self.setWindowTitle(_('Change password for {}').format(username) if username else _('Add new user'))
+ self.l = l = QFormLayout(self)
+ l.setFieldGrowthPolicy(l.AllNonFixedFieldsGrow)
+ self.uw = u = QLineEdit(self)
+ l.addRow(_('&Username:'), u)
+ if username:
+ u.setText(username)
+ u.setReadOnly(True)
+ l.addRow(QLabel(_('Set the password for this user')))
+ self.p1, self.p2 = p1, p2 = QLineEdit(self), QLineEdit(self)
+ l.addRow(_('&Password:'), p1), l.addRow(_('&Repeat password:'), p2)
+ for p in p1, p2:
+ p.setEchoMode(QLineEdit.PasswordEchoOnEdit)
+ p.setMinimumWidth(300)
+ if username:
+ p.setText(user_data[username]['pw'])
+ self.showp = sp = QCheckBox(_('&Show password'))
+ sp.stateChanged.connect(self.show_password)
+ l.addRow(sp)
+ self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ l.addRow(bb)
+ bb.accepted.connect(self.accept), bb.rejected.connect(self.reject)
+
+ def show_password(self):
+ for p in self.p1, self.p2:
+ p.setEchoMode(QLineEdit.Normal if self.showp.isChecked() else QLineEdit.PasswordEchoOnEdit)
+
+ @property
+ def username(self):
+ return self.uw.text().strip()
+
+ @property
+ def password(self):
+ return self.p1.text()
+
+ def accept(self):
+ if self.uw.isEditable():
+ un = self.username
+ if not un:
+ return error_dialog(self, _('Empty username'), _('You must enter a username'), show=True)
+ if un in self.user_data:
+ return error_dialog(self, _('Username already exists'), _(
+ 'A user witht he username {} already exists. Please choose a different username.').format(un), show=True)
+ err = validate_username(un)
+ if err:
+ return error_dialog(self, _('Username is not valid'), err, show=True)
+ p1, p2 = self.password, self.p2.text()
+ if p1 != p2:
+ return error_dialog(self, _('Password do not match'), _(
+ 'The two passwords you entered do not match!'), show=True)
+ if not p1:
+ return error_dialog(self, _('Empty password'), _(
+ 'You must enter a password for this user'), show=True)
+ err = validate_password(p1)
+ if err:
+ return error_dialog(self, _('Invalid password'), err, show=True)
+ return QDialog.accept(self)
+
+
+class User(QWidget):
+
+ changed_signal = pyqtSignal()
+
+ def __init__(self, parent=None):
+ QWidget.__init__(self, parent)
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ self.l = l = QFormLayout(self)
+ l.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
+ self.username_label = la = QLabel('')
+ l.addWidget(la)
+ self.cpb = b = QPushButton(_('Change &password'))
+ l.addWidget(b)
+ b.clicked.connect(self.change_password)
+
+ self.show_user()
+
+ def change_password(self):
+ d = NewUser(self.user_data, self, self.username)
+ if d.exec_() == d.Accepted:
+ self.user_data[self.username]['pw'] = d.password
+ self.changed_signal.emit()
+
+ def show_user(self, username=None, user_data=None):
+ self.username, self.user_data = username, user_data
+ self.cpb.setEnabled(username is not None)
+ self.username_label.setText(('
' + username) if username else '')
+
+ def sizeHint(self):
+ ans = QWidget.sizeHint(self)
+ ans.setWidth(400)
+ return ans
+
+
class Users(QWidget):
changed_signal = pyqtSignal()
def __init__(self, parent=None):
QWidget.__init__(self, parent)
+ self.l = l = QHBoxLayout(self)
+ self.lp = lp = QVBoxLayout()
+ l.addLayout(lp)
+
+ self.h = h = QHBoxLayout()
+ lp.addLayout(h)
+ self.add_button = b = QPushButton(QIcon(I('plus.png')), _('&Add user'), self)
+ b.clicked.connect(self.add_user)
+ h.addWidget(b)
+ self.remove_button = b = QPushButton(QIcon(I('minus.png')), _('&Remove user'), self)
+ b.clicked.connect(self.remove_user)
+ h.addStretch(2), h.addWidget(b)
+
+ self.user_list = w = QListWidget(self)
+ w.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
+ lp.addWidget(w)
+
+ self.user_display = u = User(self)
+ u.changed_signal.connect(self.changed_signal.emit)
+ l.addWidget(u)
def genesis(self):
self.user_data = UserManager().user_data
+ self.user_list.addItems(sorted(self.user_data, key=primary_sort_key))
+ self.user_list.setCurrentRow(0)
+ self.user_list.currentItemChanged.connect(self.current_item_changed)
+ self.current_item_changed()
+ def current_item_changed(self):
+ item = self.user_list.currentItem()
+ if item is None:
+ username = None
+ else:
+ username = item.text()
+ if username not in self.user_data:
+ username = None
+ self.display_user_data(username)
+
+ def add_user(self):
+ d = NewUser(self.user_data, parent=self)
+ if d.exec_() == d.Accepted:
+ un, pw = d.username, d.password
+ self.user_data[un] = create_user_data(pw)
+ self.user_list.insertItem(0, un)
+ self.user_list.setCurrentRow(0)
+ self.display_user_data(un)
+ self.changed_signal.emit()
+
+ def remove_user(self):
+ u = self.user_list.currentItem()
+ if u is not None:
+ self.user_list.takeItem(self.user_list.row(u))
+ un = u.text()
+ self.user_data.pop(un, None)
+ self.changed_signal.emit()
+ self.current_item_changed()
+
+ def display_user_data(self, username=None):
+ self.user_display.show_user(username, self.user_data)
# }}}
@@ -454,7 +610,8 @@ class ConfigWidget(ConfigWidgetBase):
return False
def refresh_gui(self, gui):
- pass
+ if self.server:
+ self.server.user_manager.refresh()
if __name__ == '__main__':
diff --git a/src/calibre/srv/embedded.py b/src/calibre/srv/embedded.py
index d120f0e6c3..f40ad3c933 100644
--- a/src/calibre/srv/embedded.py
+++ b/src/calibre/srv/embedded.py
@@ -45,6 +45,10 @@ class Server(object):
from calibre.utils.rapydscript import compile_srv
compile_srv()
+ @property
+ def user_manager(self):
+ return self.handler.router.ctx.user_manager
+
def start(self):
if self.current_thread is None:
try:
diff --git a/src/calibre/srv/users.py b/src/calibre/srv/users.py
index 2c41f78624..8ea5a5334f 100644
--- a/src/calibre/srv/users.py
+++ b/src/calibre/srv/users.py
@@ -42,6 +42,25 @@ def serialize_restriction(r):
return json.dumps(ans)
+def validate_username(username):
+ if re.sub(r'[a-zA-Z_0-9 ]', '', username):
+ return _('For maximum compatibility you should use only the letters A-Z,'
+ ' the numbers 0-9 and spaces or underscores in the username')
+
+
+def validate_password(pw):
+ try:
+ pw = pw.encode('ascii', 'strict')
+ except ValueError:
+ return _('The password must contain only ASCII (English) characters and symbols')
+
+
+def create_user_data(pw, readonly=False, restriction=None):
+ return {
+ 'pw':pw, 'restriction':parse_restriction(restriction or '{}'), 'readonly': readonly
+ }
+
+
class UserManager(object):
lock = RLock()
@@ -111,15 +130,10 @@ class UserManager(object):
def validate_username(self, username):
if self.has_user(username):
return _('The username %s already exists') % username
- if re.sub(r'[a-zA-Z_0-9 ]', '', username):
- return _('For maximum compatibility you should use only the letters A-Z,'
- ' the numbers 0-9 and spaces or underscores in the username')
+ return validate_username(username)
def validate_password(self, pw):
- try:
- pw = pw.encode('ascii', 'strict')
- except ValueError:
- return _('The password must contain only ASCII (English) characters and symbols')
+ return validate_password(pw)
def add_user(self, username, pw, restriction=None, readonly=False):
with self.lock:
@@ -147,9 +161,7 @@ class UserManager(object):
with self.lock:
ans = {}
for name, pw, restriction, readonly in self.conn.cursor().execute('SELECT name,pw,restriction,readonly FROM users'):
- ans[name] = {
- 'pw':pw, 'restriction':parse_restriction(restriction), 'readonly': readonly.lower() == 'y'
- }
+ ans[name] = create_user_data(pw, readonly.lower() == 'y', restriction)
return ans
@user_data.setter
@@ -164,8 +176,11 @@ class UserManager(object):
if self.conn.changes() > 0:
continue
c.execute('INSERT INTO USERS (name, pw, restriction, readonly)', name, data['pw'], res, r)
- self._restrictions.clear()
- self._readonly.clear()
+ self.refresh()
+
+ def refresh(self):
+ self._restrictions.clear()
+ self._readonly.clear()
def is_readonly(self, username):
with self.lock: