From 97e4707f72d853da05f79249fbc6c6eee73773ec Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 25 Apr 2021 10:42:41 +0200 Subject: [PATCH] user list column value and tags editor (#1938) Bugfixes for massedit in user list (#1938) Changed no. of debug messages --- cps/admin.py | 122 ++++++++++------- cps/services/gmail.py | 2 + cps/static/css/main.css | 4 + cps/static/js/table.js | 245 ++++++++++++++++++++++------------ cps/tasks/mail.py | 6 +- cps/templates/user_table.html | 46 ++++--- 6 files changed, 276 insertions(+), 149 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 5b18bb64..ba82d2c2 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -225,11 +225,23 @@ def edit_user_table(): languages = calibre_db.speaking_language() translations = babel.list_translations() + [LC('en')] allUser = ub.session.query(ub.User) + tags = calibre_db.session.query(db.Tags)\ + .join(db.books_tags_link)\ + .join(db.Books)\ + .filter(calibre_db.common_filters()) \ + .group_by(text('books_tags_link.tag'))\ + .order_by(db.Tags.name).all() + if config.config_restricted_column: + custom_values = calibre_db.session.query(db.cc_classes[config.config_restricted_column]).all() + else: + custom_values = [] if not config.config_anonbrowse: allUser = allUser.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) return render_title_template("user_table.html", users=allUser.all(), + tags=tags, + custom_values=custom_values, translations=translations, languages=languages, visiblility=visibility, @@ -238,6 +250,7 @@ def edit_user_table(): title=_(u"Edit Users"), page="usertable") + @admi.route("/ajax/listusers") @login_required @admin_required @@ -332,7 +345,7 @@ def table_get_locale(): def table_get_default_lang(): languages = calibre_db.speaking_language() ret = list() - ret.append({'value':'all','text':_('Show All')}) + ret.append({'value': 'all', 'text': _('Show All')}) for lang in languages: ret.append({'value': lang.lang_code, 'text': lang.name}) return json.dumps(ret) @@ -358,56 +371,55 @@ def edit_list_user(param): vals['field_index'] = vals['field_index'][0] if 'value' in vals: vals['value'] = vals['value'][0] - else: + elif not ('value[]' in vals): return "" for user in users: try: - vals['value'] = vals['value'].strip() - if param == 'name': - if user.name == "Guest": - raise Exception(_("Guest Name can't be changed")) - user.name = check_username(vals['value']) - elif param =='email': - user.email = check_email(vals['value']) - elif param == 'kindle_mail': - user.kindle_mail = valid_email(vals['value']) if vals['value'] else "" - elif param.endswith('role'): - if user.name == "Guest" and int(vals['field_index']) in \ - [constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]: - raise Exception(_("Guest can't have this role")) - if vals['value'] == 'true': - user.role |= int(vals['field_index']) + if param in ['denied_tags', 'allowed_tags', 'allowed_column_value', 'denied_column_value']: + if 'value[]' in vals: + setattr(user, param, prepare_tags(user, vals['action'][0], param, vals['value[]'])) else: - if int(vals['field_index']) == constants.ROLE_ADMIN: - if not ub.session.query(ub.User).\ - filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, - ub.User.id != user.id).count(): - return Response(json.dumps([{'type': "danger", - 'message':_(u"No admin user remaining, can't remove admin role", - nick=user.name)}]), mimetype='application/json') - user.role &= ~int(vals['field_index']) - elif param.startswith('sidebar'): - if user.name == "Guest" and int(vals['field_index']) == constants.SIDEBAR_READ_AND_UNREAD: - raise Exception(_("Guest can't have this view")) - if vals['value'] == 'true': - user.sidebar_view |= int(vals['field_index']) - else: - user.sidebar_view &= ~int(vals['field_index']) - elif param == 'denied_tags': - user.denied_tags = vals['value'] - elif param == 'allowed_tags': - user.allowed_tags = vals['value'] - elif param == 'allowed_column_value': - user.allowed_column_value = vals['value'] - elif param == 'denied_column_value': - user.denied_column_value = vals['value'] - elif param == 'locale': - if user.name == "Guest": - raise Exception(_("Guest's Locale is determined automatically and can't be set")) - user.locale = vals['value'] - elif param == 'default_language': - user.default_language = vals['value'] + setattr(user, param, vals['value'].strip()) + else: + vals['value'] = vals['value'].strip() + if param == 'name': + if user.name == "Guest": + raise Exception(_("Guest Name can't be changed")) + user.name = check_username(vals['value']) + elif param =='email': + user.email = check_email(vals['value']) + elif param == 'kindle_mail': + user.kindle_mail = valid_email(vals['value']) if vals['value'] else "" + elif param.endswith('role'): + if user.name == "Guest" and int(vals['field_index']) in \ + [constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]: + raise Exception(_("Guest can't have this role")) + if vals['value'] == 'true': + user.role |= int(vals['field_index']) + else: + if int(vals['field_index']) == constants.ROLE_ADMIN: + if not ub.session.query(ub.User).\ + filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, + ub.User.id != user.id).count(): + return Response(json.dumps([{'type': "danger", + 'message':_(u"No admin user remaining, can't remove admin role", + nick=user.name)}]), mimetype='application/json') + user.role &= ~int(vals['field_index']) + elif param.startswith('sidebar'): + if user.name == "Guest" and int(vals['field_index']) == constants.SIDEBAR_READ_AND_UNREAD: + raise Exception(_("Guest can't have this view")) + if vals['value'] == 'true': + user.sidebar_view |= int(vals['field_index']) + else: + user.sidebar_view &= ~int(vals['field_index']) + elif param == 'locale': + if user.name == "Guest": + raise Exception(_("Guest's Locale is determined automatically and can't be set")) + user.locale = vals['value'] + elif param == 'default_language': + user.default_language = vals['value'] except Exception as ex: + log.debug_or_exception(ex) return str(ex), 400 ub.session_commit() return "" @@ -483,6 +495,8 @@ def load_dialogtexts(element_id): texts["main"] = _('Are you sure you want to change visible book languages for selected user(s)?') elif element_id == "role": texts["main"] = _('Are you sure you want to change the selected role for the selected user(s)?') + elif element_id == "restrictions": + texts["main"] = _('Are you sure you want to change the selected restrictions for the selected user(s)?') elif element_id == "sidebar_view": texts["main"] = _('Are you sure you want to change the selected visibility restrictions for the selected user(s)?') return json.dumps(texts) @@ -629,6 +643,22 @@ def restriction_deletion(element, list_func): return ','.join(elementlist) +def prepare_tags(user, action, tags_name, id_list): + if "tags" in tags_name: + tags = calibre_db.session.query(db.Tags).filter(db.Tags.id.in_(id_list)).all() + new_tags_list = [x.name for x in tags] + else: + tags = calibre_db.session.query(db.cc_classes[config.config_restricted_column])\ + .filter(db.cc_classes[config.config_restricted_column].id.in_(id_list)).all() + new_tags_list = [x.value for x in tags] + saved_tags_list = user.__dict__[tags_name].split(",") if len(user.__dict__[tags_name]) else [] + if action == "remove": + saved_tags_list = [x for x in saved_tags_list if x not in new_tags_list] + else: + saved_tags_list.extend(x for x in new_tags_list if x not in saved_tags_list) + return ",".join(saved_tags_list) + + @admi.route("/ajax/addrestriction/", defaults={"user_id": 0}, methods=['POST']) @admi.route("/ajax/addrestriction//", methods=['POST']) @login_required diff --git a/cps/services/gmail.py b/cps/services/gmail.py index f85d56b0..9380121a 100644 --- a/cps/services/gmail.py +++ b/cps/services/gmail.py @@ -61,6 +61,7 @@ def get_user_info(credentials): return user_info.get('email', "") def send_messsage(token, msg): + log.debug("Start sending email via Gmail") creds = Credentials( token=token['token'], refresh_token=token['refresh_token'], @@ -79,3 +80,4 @@ def send_messsage(token, msg): body = {'raw': raw} (service.users().messages().send(userId='me', body=body).execute()) + log.debug("Email send successfully via Gmail") diff --git a/cps/static/css/main.css b/cps/static/css/main.css index e97497de..adbfbfdf 100644 --- a/cps/static/css/main.css +++ b/cps/static/css/main.css @@ -15,6 +15,10 @@ body { overflow: hidden; } +.myselect { + overflow: visible !important; +} + #main { position: absolute; width: 100%; diff --git a/cps/static/js/table.js b/cps/static/js/table.js index a15b2e45..2ccee233 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -21,7 +21,6 @@ var selections = []; $(function() { - $("#books-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table", function (e, rowsAfter, rowsBefore) { var rows = rowsAfter; @@ -453,27 +452,11 @@ $(function() { }); }, onPostHeader () { - deactivateHeaderButtons(); + move_header_elements(); }, onLoadSuccess: function () { loadSuccess(); - var element = $(".header_select"); - element.each(function() { - var item = $(this).parent(); - var parent = item.parent().parent(); - if (parent.prop('nodeName') === "TH") { - item.prependTo(parent); - } - }); - var element = $(".form-check"); - element.each(function() { - var item = $(this).parent(); - var parent = item.parent().parent(); - if (parent.prop('nodeName') === "TH") { - item.prependTo(parent); - } - }); - + move_header_elements(); }, onColumnSwitch: function () { var visible = $("#user-table").bootstrapTable("getVisibleColumns"); @@ -493,56 +476,10 @@ $(function() { url: window.location.pathname + "/../../ajax/user_table_settings", data: "{" + st + "}", }); + handle_header_buttons(); }, }); - $("#user_delete_selection").click(function() { - $("#user-table").bootstrapTable("uncheckAll"); - }); - $("#select_locale").on("change",function() { - selectHeader(this, "locale"); - }); - $("#select_default_language").on("change",function() { - selectHeader(this, "default_language"); - }); - $(".check_head").on("change",function() { - var val = $(this).val() === "on"; - var name = $(this).data("name"); - var data = $(this).data("val"); - checkboxHeader(val, name, data); - }); - $(".button_head").on("click",function() { - var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); - confirmDialog( - "btndeluser", - "GeneralDeleteModal", - 0, - function() { - $.ajax({ - method:"post", - url: window.location.pathname + "/../../ajax/deleteuser", - data: {"userid": result}, - success: function (data) { - selections = selections.filter( ( el ) => !result.includes( el ) ); - // selections = selections.filter(item => item !== userId); - handleListServerResponse(data); - }, - error: function (data) { - handleListServerResponse({type:"danger", message:data.responseText}) - }, - }); - } - ); - }); - function user_handle (userId) { - $.ajax({ - method:"post", - url: window.location.pathname + "/../../ajax/deleteuser", - data: {"userid":userId} - }); - $("#user-table").bootstrapTable("refresh"); - } - $("#user-table").on("click-cell.bs.table", function (field, value, row, $element) { if (value === "denied_column_value") { ConfirmDialog("btndeluser", "GeneralDeleteModal", $element.id, user_handle); @@ -562,7 +499,8 @@ $(function() { }); var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference"; selections = window._[func](selections, ids); - if (selections.length < 1) { + handle_header_buttons(); + /*if (selections.length < 1) { $("#user_delete_selection").addClass("disabled"); $("#user_delete_selection").attr("aria-disabled", true); $(".check_head").attr("aria-disabled", true); @@ -570,6 +508,12 @@ $(function() { $(".check_head").prop('checked', false); $(".button_head").attr("aria-disabled", true); $(".button_head").addClass("disabled"); + $(".multi_head").attr("aria-disabled", true); + $(".multi_head").addClass("hidden"); + $(".multi_selector").attr("aria-disabled", true); + $(".multi_selector").attr("disabled", true); + $('.multi_selector').selectpicker('deselectAll'); + $('.multi_selector').selectpicker('refresh'); $(".header_select").attr("disabled", true); } else { $("#user_delete_selection").removeClass("disabled"); @@ -578,12 +522,47 @@ $(function() { $(".check_head").removeAttr("disabled"); $(".button_head").attr("aria-disabled", false); $(".button_head").removeClass("disabled"); + $(".multi_head").attr("aria-disabled", false); + $(".multi_head").removeClass("hidden"); + $(".multi_selector").attr("aria-disabled", false); + $(".multi_selector").removeAttr("disabled"); + $('.multi_selector').selectpicker('refresh'); $(".header_select").removeAttr("disabled"); - } + }*/ }); }); - +function handle_header_buttons () { + if (selections.length < 1) { + $("#user_delete_selection").addClass("disabled"); + $("#user_delete_selection").attr("aria-disabled", true); + $(".check_head").attr("aria-disabled", true); + $(".check_head").attr("disabled", true); + $(".check_head").prop('checked', false); + $(".button_head").attr("aria-disabled", true); + $(".button_head").addClass("disabled"); + $(".multi_head").attr("aria-disabled", true); + $(".multi_head").addClass("hidden"); + $(".multi_selector").attr("aria-disabled", true); + $(".multi_selector").attr("disabled", true); + $('.multi_selector').selectpicker('deselectAll'); + $('.multi_selector').selectpicker('refresh'); + $(".header_select").attr("disabled", true); + } else { + $("#user_delete_selection").removeClass("disabled"); + $("#user_delete_selection").attr("aria-disabled", false); + $(".check_head").attr("aria-disabled", false); + $(".check_head").removeAttr("disabled"); + $(".button_head").attr("aria-disabled", false); + $(".button_head").removeClass("disabled"); + $(".multi_head").attr("aria-disabled", false); + $(".multi_head").removeClass("hidden"); + $(".multi_selector").attr("aria-disabled", false); + $(".multi_selector").removeAttr("disabled"); + $('.multi_selector').selectpicker('refresh'); + $(".header_select").removeAttr("disabled"); + } +} /* Function for deleting domain restrictions */ function TableActions (value, row) { return [ @@ -644,6 +623,12 @@ function checkboxFormatter(value, row, index){ function loadSuccess() { var guest = $(".editable[data-name='name'][data-value='Guest']"); guest.editable("disable"); + $("input:radio.check_head:checked").each(function() { + $(this).prop('checked', false); + }); + $(".header_select").each(function() { + $(this).prop("selectedIndex", 0); + }); $(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").editable("disable"); $(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").hide(); $("input[data-name='admin_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); @@ -653,7 +638,99 @@ function loadSuccess() { $(".user-remove[data-pk='"+guest.data("pk")+"']").hide(); } -function handleListServerResponse (data, disableButtons) { +function move_header_elements() { + $(".header_select").each(function() { + var item = $(this).parent(); + var parent = item.parent().parent(); + if (parent.prop('nodeName') === "TH") { + item.prependTo(parent); + } + }); + $(".form-check").each(function() { + var item = $(this).parent(); + var parent = item.parent().parent(); + if (parent.prop('nodeName') === "TH") { + item.prependTo(parent); + } + }); + $(".multi_select").each(function() { + var item = $(this); + var parent = item.parent().parent(); + if (parent.prop('nodeName') === "TH") { + item.prependTo(parent); + item.addClass("myselect"); + } + }); + $(".multi_selector").selectpicker(); + + // Functions have to be here, otherwise the callbacks are not fired if visible columns are changed + $(".multi_head").on("click",function() { + var val = $(this).data("set"); + var field = $(this).data("name"); + var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); + var values = $("#" + field).val(); + confirmDialog( + "restrictions", + "GeneralChangeModal", + 0, + function() { + $.ajax({ + method:"post", + url: window.location.pathname + "/../../ajax/editlistusers/" + field, + data: {"pk": result, "value": values, "action": val}, + success: function (data) { + handleListServerResponse(data); + }, + error: function (data) { + handleListServerResponse({type:"danger", message:data.responseText}) + }, + }); + } + ); + }); + + $("#user_delete_selection").click(function() { + $("#user-table").bootstrapTable("uncheckAll"); + }); + $("#select_locale").on("change",function() { + selectHeader(this, "locale"); + }); + $("#select_default_language").on("change",function() { + selectHeader(this, "default_language"); + }); + $(".check_head").on("change",function() { + var val = $(this).data("set"); + var name = $(this).data("name"); + var data = $(this).data("val"); + checkboxHeader(val, name, data); + }); + + $(".button_head").on("click",function() { + var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); + confirmDialog( + "btndeluser", + "GeneralDeleteModal", + 0, + function() { + $.ajax({ + method:"post", + url: window.location.pathname + "/../../ajax/deleteuser", + data: {"userid": result}, + success: function (data) { + selections = selections.filter( ( el ) => !result.includes( el ) ); + handleListServerResponse(data); + }, + error: function (data) { + handleListServerResponse({type:"danger", message:data.responseText}) + }, + }); + } + ); + }); + +} + +function handleListServerResponse (data) { $("#flash_success").remove(); $("#flash_danger").remove(); if (!jQuery.isEmptyObject(data)) { @@ -679,19 +756,6 @@ function checkboxChange(checkbox, userId, field, field_index) { }); } -function deactivateHeaderButtons() { - if (selections.length < 1) { - $("#user_delete_selection").addClass("disabled"); - $("#user_delete_selection").attr("aria-disabled", true); - $(".check_head").attr("aria-disabled", true); - $(".check_head").attr("disabled", true); - $(".check_head").prop('checked', false); - $(".button_head").attr("aria-disabled", true); - $(".button_head").addClass("disabled"); - $(".header_select").attr("disabled", true); - } -} - function selectHeader(element, field) { if (element.value !== "None") { confirmDialog(element.id, "GeneralChangeModal", 0, function () { @@ -705,6 +769,8 @@ function selectHeader(element, field) { }, success: handleListServerResponse, }); + },function() { + $(element).prop("selectedIndex", 0); }); } } @@ -723,6 +789,10 @@ function checkboxHeader(CheckboxState, field, field_index) { handleListServerResponse (data, true) }, }); + },function() { + $("input:radio.check_head:checked").each(function() { + $(this).prop('checked', false); + }); }); } @@ -758,3 +828,12 @@ function queryParams(params) function storeLocation() { window.sessionStorage.setItem("back", window.location.pathname); } + +function user_handle (userId) { + $.ajax({ + method:"post", + url: window.location.pathname + "/../../ajax/deleteuser", + data: {"userid":userId} + }); + $("#user-table").bootstrapTable("refresh"); +} diff --git a/cps/tasks/mail.py b/cps/tasks/mail.py index e8b3ebf3..f19231ec 100644 --- a/cps/tasks/mail.py +++ b/cps/tasks/mail.py @@ -115,7 +115,6 @@ class TaskEmail(CalibreTask): self.results = dict() def prepare_message(self): - log.debug("prepare email message for sending") message = MIMEMultipart() message['to'] = self.recipent message['from'] = self.settings["mail_from"] @@ -171,11 +170,10 @@ class TaskEmail(CalibreTask): # redirect output to logfile on python2 on python3 debugoutput is caught with overwritten # _print_debug function if sys.version_info < (3, 0): - log.debug("Redirect output on python2 for email") org_smtpstderr = smtplib.stderr smtplib.stderr = logger.StderrLogger('worker.smtp') - log.debug("Start send email") + log.debug("Start sending email") if use_ssl == 2: self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"], timeout=timeout) @@ -188,7 +186,6 @@ class TaskEmail(CalibreTask): if use_ssl == 1: self.asyncSMTP.starttls() if self.settings["mail_password"]: - log.debug("Login to email server") self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password"])) # Convert message to something to send @@ -196,7 +193,6 @@ class TaskEmail(CalibreTask): gen = Generator(fp, mangle_from_=False) gen.flatten(msg) - log.debug("Sending email") self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipent, fp.getvalue()) self.asyncSMTP.quit() self._handleSuccess() diff --git a/cps/templates/user_table.html b/cps/templates/user_table.html index 6cfa09cf..b174e481 100644 --- a/cps/templates/user_table.html +++ b/cps/templates/user_table.html @@ -1,5 +1,5 @@ {% extends "layout.html" %} -{% macro user_table_row(parameter, edit_text, show_text, validate, button=False, id=0) -%} +{% macro user_table_row(parameter, edit_text, show_text, validate, elements=False) -%} - {% if button %} -
+ {% if elements %} +
+ +
+
+ +
+
+ +
+
+
{% endif %} {{ show_text }} @@ -25,14 +39,14 @@ data-formatter="checkboxFormatter">
- + + {{_('Deny')}} +
- + + {{_('Allow')}} +
{{show_text}} @@ -51,6 +65,7 @@ {% if validate %}data-edit-validate="{{ _('This Field is Required') }}"{% endif %}>