Work on GUI for managing server users

This commit is contained in:
Kovid Goyal 2017-04-21 09:07:06 +05:30
parent 5abf1a5fc8
commit 1b3d29e7f8
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 193 additions and 17 deletions

View File

@ -7,16 +7,18 @@ import time
from PyQt5.Qt import ( from PyQt5.Qt import (
QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout,
QHBoxLayout, QLabel, QLineEdit, QPlainTextEdit, QPushButton, QScrollArea, QSize, QHBoxLayout, QIcon, QLabel, QLineEdit, QListWidget, QPlainTextEdit, QPushButton,
QSizePolicy, QSpinBox, Qt, QTabWidget, QTimer, QUrl, QVBoxLayout, QWidget, QScrollArea, QSize, QSizePolicy, QSpinBox, Qt, QTabWidget, QTimer, QUrl,
pyqtSignal QVBoxLayout, QWidget, pyqtSignal
) )
from calibre import as_unicode from calibre import as_unicode
from calibre.gui2 import config, error_dialog, info_dialog, open_url, warning_dialog from calibre.gui2 import config, error_dialog, info_dialog, open_url, warning_dialog
from calibre.gui2.preferences import AbortCommit, ConfigWidgetBase, test_widget from calibre.gui2.preferences import AbortCommit, ConfigWidgetBase, test_widget
from calibre.srv.opts import change_settings, options, server_config 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 {{{ # Advanced {{{
@ -273,16 +275,170 @@ class MainTab(QWidget): # {{{
# Users {{{ # 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(('<h2>' + username) if username else '')
def sizeHint(self):
ans = QWidget.sizeHint(self)
ans.setWidth(400)
return ans
class Users(QWidget): class Users(QWidget):
changed_signal = pyqtSignal() changed_signal = pyqtSignal()
def __init__(self, parent=None): def __init__(self, parent=None):
QWidget.__init__(self, parent) 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): def genesis(self):
self.user_data = UserManager().user_data 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 return False
def refresh_gui(self, gui): def refresh_gui(self, gui):
pass if self.server:
self.server.user_manager.refresh()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -45,6 +45,10 @@ class Server(object):
from calibre.utils.rapydscript import compile_srv from calibre.utils.rapydscript import compile_srv
compile_srv() compile_srv()
@property
def user_manager(self):
return self.handler.router.ctx.user_manager
def start(self): def start(self):
if self.current_thread is None: if self.current_thread is None:
try: try:

View File

@ -42,6 +42,25 @@ def serialize_restriction(r):
return json.dumps(ans) 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): class UserManager(object):
lock = RLock() lock = RLock()
@ -111,15 +130,10 @@ class UserManager(object):
def validate_username(self, username): def validate_username(self, username):
if self.has_user(username): if self.has_user(username):
return _('The username %s already exists') % username return _('The username %s already exists') % username
if re.sub(r'[a-zA-Z_0-9 ]', '', username): return validate_username(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(self, pw): def validate_password(self, pw):
try: return validate_password(pw)
pw = pw.encode('ascii', 'strict')
except ValueError:
return _('The password must contain only ASCII (English) characters and symbols')
def add_user(self, username, pw, restriction=None, readonly=False): def add_user(self, username, pw, restriction=None, readonly=False):
with self.lock: with self.lock:
@ -147,9 +161,7 @@ class UserManager(object):
with self.lock: with self.lock:
ans = {} ans = {}
for name, pw, restriction, readonly in self.conn.cursor().execute('SELECT name,pw,restriction,readonly FROM users'): for name, pw, restriction, readonly in self.conn.cursor().execute('SELECT name,pw,restriction,readonly FROM users'):
ans[name] = { ans[name] = create_user_data(pw, readonly.lower() == 'y', restriction)
'pw':pw, 'restriction':parse_restriction(restriction), 'readonly': readonly.lower() == 'y'
}
return ans return ans
@user_data.setter @user_data.setter
@ -164,8 +176,11 @@ class UserManager(object):
if self.conn.changes() > 0: if self.conn.changes() > 0:
continue continue
c.execute('INSERT INTO USERS (name, pw, restriction, readonly)', name, data['pw'], res, r) c.execute('INSERT INTO USERS (name, pw, restriction, readonly)', name, data['pw'], res, r)
self._restrictions.clear() self.refresh()
self._readonly.clear()
def refresh(self):
self._restrictions.clear()
self._readonly.clear()
def is_readonly(self, username): def is_readonly(self, username):
with self.lock: with self.lock: