New modals for magazine and issue edits (thanks @txzeenath)

This commit is contained in:
phil.borman@gmail.com 2025-06-26 18:35:30 +02:00
parent 6c1b0f8d5e
commit 164b37060b
3 changed files with 429 additions and 5 deletions

View File

@ -213,12 +213,87 @@
}
},
callback: function (result) {
if (result) { document.getElementById("mark_issues").submit(); }
if (result) { submitFormAjax(); }
}
});
return false;
}
else { document.getElementById("mark_issues").submit(); }
else { submitFormAjax(); }
}
function submitFormAjax() {
var form = document.getElementById("mark_issues");
var formData = new FormData(form);
// Show loading modal
bootbox.dialog({
message: '<div class="text-center"><i class="fa fa-spinner fa-spin fa-2x"></i><br><br>Processing...</div>',
size: 'small',
backdrop: false
});
// Convert FormData to URLSearchParams for the AJAX call
var params = new URLSearchParams();
var formDataEntries = formData.entries();
for (var pair of formDataEntries) {
params.append(pair[0], pair[1]);
}
// Make AJAX call to the new endpoint
fetch('mark_issues_ajax', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params
})
.then(response => response.json())
.then(data => {
// Close loading modal
bootbox.hideAll();
// Show result modal
var message = '<div class="alert alert-info"><strong>Summary:</strong><br>' + data.summary + '</div>';
if (data.passed > 0 || data.failed > 0) {
message += '<div class="row">';
message += '<div class="col-md-6"><span class="label label-success">Successful: ' + data.passed + '</span></div>';
if (data.failed > 0) {
message += '<div class="col-md-6"><span class="label label-danger">Failed: ' + data.failed + '</span></div>';
}
message += '</div>';
}
bootbox.dialog({
title: 'Operation Complete',
message: message,
buttons: {
ok: {
label: 'OK',
className: 'btn-primary',
callback: function() {
// Refresh the page to show updated data
window.location.reload();
}
}
}
});
})
.catch(error => {
// Close loading modal
bootbox.hideAll();
// Show error modal
bootbox.dialog({
title: 'Error',
message: '<div class="alert alert-danger">An error occurred: ' + error.message + '</div>',
buttons: {
ok: {
label: 'OK',
className: 'btn-danger'
}
}
});
});
}
</script>
</%def>

View File

@ -275,12 +275,86 @@
}
},
callback: function (result) {
if (result) { document.getElementById("mark_magazines").submit(); }
if (result) { submitMagazinesFormAjax(); }
}
});
return false;
}
else { document.getElementById("mark_magazines").submit(); }
else { submitMagazinesFormAjax(); }
}
function submitMagazinesFormAjax() {
var form = document.getElementById("mark_magazines");
var formData = new FormData(form);
// Show loading modal
bootbox.dialog({
message: '<div class="text-center"><i class="fa fa-spinner fa-spin fa-2x"></i><br><br>Processing...</div>',
size: 'small',
backdrop: false
});
// Convert FormData to URLSearchParams for the AJAX call
var params = new URLSearchParams();
var formDataEntries = formData.entries();
for (var pair of formDataEntries) {
params.append(pair[0], pair[1]);
}
// Make AJAX call to the new endpoint
fetch('mark_magazines_ajax', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params
})
.then(response => response.json())
.then(data => {
// Close loading modal
bootbox.hideAll();
// Show result modal
var message = '<div class="alert alert-info"><strong>Summary:</strong><br>' + data.summary + '</div>';
if (data.passed > 0 || data.failed > 0) {
message += '<div class="row">';
message += '<div class="col-md-6"><span class="label label-success">Successful: ' + data.passed + '</span></div>';
if (data.failed > 0) {
message += '<div class="col-md-6"><span class="label label-danger">Failed: ' + data.failed + '</span></div>';
}
message += '</div>';
}
bootbox.dialog({
title: 'Operation Complete',
message: message,
buttons: {
ok: {
label: 'OK',
className: 'btn-primary',
callback: function() {
// Refresh the page to show updated data
window.location.reload();
}
}
}
});
})
.catch(error => {
// Close loading modal
bootbox.hideAll();
// Show error modal
bootbox.dialog({
title: 'Error',
message: '<div class="alert alert-danger">An error occurred: ' + error.message + '</div>',
buttons: {
ok: {
label: 'OK',
className: 'btn-danger'
}
}
});
});
}
</script>
</%def>

View File

@ -1016,6 +1016,7 @@ class WebInterface:
self.check_permitted(lazylibrarian.perm_admin)
db = database.DBConnection()
try:
res = ''
match = db.match('SELECT * from users where UserName=?', (kwargs['user'],))
if match:
subs = db.select('SELECT Type,WantID from subscribers WHERE UserID=?', (match['userid'],))
@ -1040,7 +1041,7 @@ class WebInterface:
'theme': '', 'hc_id': '', 'hc_token': ''})
finally:
db.close()
return res
return res
@cherrypy.expose
def admin_users(self, **kwargs):
@ -1209,6 +1210,7 @@ class WebInterface:
self.label_thread('PASSWORD_RESET')
logger = logging.getLogger(__name__)
res = {}
msg = ''
remote_ip = cherrypy.request.remote.ip
db = database.DBConnection()
try:
@ -6070,6 +6072,151 @@ class WebInterface:
else:
raise cherrypy.HTTPRedirect("magazines")
@cherrypy.expose
@cherrypy.tools.json_out()
def mark_issues_ajax(self, action=None, **args):
self.check_permitted(lazylibrarian.perm_status)
logger = logging.getLogger(__name__)
db = database.DBConnection()
passed = 0
failed = 0
try:
title = ''
args.pop('book_table_length', None)
if action:
for item in args:
issue = db.match('SELECT IssueFile,Title,IssueDate,Cover from issues WHERE IssueID=?', (item,))
if issue:
issue = dict(issue)
title = issue['Title']
issuefile = issue['IssueFile']
if not issuefile or not path_exists(issuefile):
logger.error(f"No IssueFile found for IssueID {item}")
failed += 1
continue
if 'reCover' in action and issuefile:
coverfile = create_mag_cover(issuefile, refresh=True,
pagenum=check_int(action[-1], 1))
if coverfile:
myhash = uuid.uuid4().hex
hashname = os.path.join(DIRS.CACHEDIR, 'magazine', f'{myhash}.jpg')
copyfile(coverfile, hashname)
setperm(hashname)
control_value_dict = {"IssueFile": issue['IssueFile']}
newcover = f'cache/magazine/{myhash}.jpg'
new_value_dict = {"Cover": newcover}
db.upsert("Issues", new_value_dict, control_value_dict)
latest = db.match("select Title,LatestCover,IssueDate from magazines "
"where title=? COLLATE NOCASE", (title,))
if latest:
title = latest['Title']
if latest['IssueDate'] == issue['IssueDate'] and latest['LatestCover'] != newcover:
db.action("UPDATE magazines SET LatestCover=? "
"WHERE Title=? COLLATE NOCASE", (newcover, title))
issue['Cover'] = newcover
issue['CoverFile'] = coverfile # for updating calibre cover
if CONFIG['IMP_CALIBREDB'] and CONFIG.get_bool('IMP_CALIBRE_MAGAZINE'):
self.update_calibre_issue_cover(issue)
passed += 1
else:
failed += 1
logger.warning(f"No coverfile created for IssueID {item} {issuefile}")
if action == 'tag' and issuefile:
logger.debug(f"Tagging {issuefile}")
res = tag_issue(issuefile, title, issue['IssueDate'])
if not res:
failed += 1
else:
passed += 1
if CONFIG.get_bool('IMP_MAGOPF'):
logger.debug(f"Writing opf for {issuefile}")
entry = db.match('SELECT Language FROM magazines where Title=?', (title,))
_, _ = lazylibrarian.postprocess.create_mag_opf(issuefile, title,
issue['IssueDate'], item,
language=entry[0],
overwrite=True)
if action == 'coverswap' and issuefile:
coverfile = None
if CONFIG['MAG_COVERSWAP']:
params = [CONFIG['MAG_COVERSWAP'], issuefile]
logger.debug(f"Coverswap {params}")
try:
res = subprocess.check_output(params, stderr=subprocess.STDOUT)
logger.info(res)
coverfile = create_mag_cover(issuefile, refresh=True, pagenum=1)
except subprocess.CalledProcessError as e:
logger.warning(e.output)
else:
res = coverswap(issuefile, 2) # cover from page 2 (counted from 1)
if res:
coverfile = create_mag_cover(issuefile, refresh=True, pagenum=1)
if coverfile:
myhash = uuid.uuid4().hex
hashname = os.path.join(DIRS.CACHEDIR, 'magazine', f'{myhash}.jpg')
copyfile(coverfile, hashname)
setperm(hashname)
control_value_dict = {"IssueFile": issuefile}
newcover = f'cache/magazine/{myhash}.jpg'
new_value_dict = {"Cover": newcover}
db.upsert("Issues", new_value_dict, control_value_dict)
latest = db.match("select Title,LatestCover,IssueDate from magazines "
"where title=? COLLATE NOCASE", (title,))
if latest:
title = latest['Title']
if latest['IssueDate'] == issue['IssueDate'] and latest['LatestCover'] != newcover:
db.action("UPDATE magazines SET LatestCover=? "
"WHERE Title=? COLLATE NOCASE", (newcover, title))
issue['Cover'] = newcover
issue['CoverFile'] = coverfile # for updating calibre cover
if CONFIG['IMP_CALIBREDB'] and CONFIG.get_bool('IMP_CALIBRE_MAGAZINE'):
self.update_calibre_issue_cover(issue)
passed += 1
else:
failed += 1
logger.warning(f"No coverfile created for IssueID {item} {issuefile}")
if action == "Delete" and issuefile:
result = self.delete_issue(issuefile)
if result:
passed += 1
logger.info(f'Issue {issue["IssueDate"]} of {issue["Title"]} deleted from disc')
if CONFIG['IMP_CALIBREDB'] and CONFIG.get_bool('IMP_CALIBRE_MAGAZINE'):
self.delete_from_calibre(issue)
else:
failed += 1
if action == "Remove" or action == "Delete":
db.action('DELETE from issues WHERE IssueID=?', (item,))
logger.info(f'Issue {issue["IssueDate"]} of {issue["Title"]} removed from database')
_ = self.mag_set_latest(title)
passed += 1
finally:
db.close()
logger.debug(f"{action.title()}: Pass {passed}, Fail {failed}")
# Return JSON response instead of redirect
total = passed + failed
summary = f"Operation '{action}' completed."
if total > 0:
summary += f" {passed} successful"
if failed > 0:
summary += f", {failed} failed"
summary += f" out of {total} total items."
else:
summary += " No items were processed."
return {
'success': True,
'action': action,
'passed': passed,
'failed': failed,
'total': total,
'title': title,
'summary': summary
}
@staticmethod
def mag_set_latest(title):
# Set magazine_issuedate to issuedate of most recent issue we have
@ -6256,6 +6403,134 @@ class WebInterface:
logger.debug(f"{action.title()}: Pass {passed}, Fail {failed}")
raise cherrypy.HTTPRedirect("magazines")
@cherrypy.expose
@cherrypy.tools.json_out()
def mark_magazines_ajax(self, action=None, **args):
self.check_permitted(lazylibrarian.perm_status)
logger = logging.getLogger(__name__)
db = database.DBConnection()
try:
args.pop('book_table_length', None)
passed = 0
failed = 0
for item in args:
title = item
if action == "Paused" or action == "Active":
control_value_dict = {"Title": title}
new_value_dict = {"Status": action}
db.upsert("magazines", new_value_dict, control_value_dict)
logger.info(f'Status of magazine {title} changed to {action}')
if action == "Delete":
issues = db.select('SELECT * from issues WHERE Title=?', (title,))
logger.debug(f'Deleting magazine {title} from disc')
issuedir = ''
for issue in issues: # delete all issues of this magazine
result = self.delete_issue(issue['IssueFile'])
if result:
logger.debug(f'Issue {issue["IssueFile"]} deleted from disc')
if CONFIG['IMP_CALIBREDB'] and CONFIG.get_bool('IMP_CALIBRE_MAGAZINE'):
self.delete_from_calibre(issue)
issuedir = os.path.dirname(issue['IssueFile'])
else:
logger.debug(f'Failed to delete {issue["IssueFile"]}')
# if the directory is now empty, delete that too
if issuedir and CONFIG.get_bool('MAG_DELFOLDER'):
magdir = os.path.dirname(issuedir)
try:
os.rmdir(syspath(magdir))
logger.debug(f'Magazine directory {magdir} deleted from disc')
except OSError:
logger.debug(f'Magazine directory {magdir} is not empty')
logger.info(f'Magazine {title} deleted from disc')
if action == 'tag':
issues = db.select('SELECT * from issues WHERE Title=?', (title,))
mag = db.match('SELECT Language FROM magazines where Title=?', (title,))
for issue in issues:
logger.debug(f"Tagging {issue['IssueFile']}")
res = tag_issue(issue['IssueFile'], title, issue["IssueDate"])
if not res:
failed += 1
else:
passed += 1
if CONFIG.get_bool('IMP_MAGOPF'):
logger.debug(f"Writing opf for {issue['IssueFile']}")
_, _ = lazylibrarian.postprocess.create_mag_opf(issue['IssueFile'], title,
issue["IssueDate"],
issue["IssueID"],
language=mag[0],
overwrite=True)
if action == "Remove" or action == "Delete":
db.action('DELETE from magazines WHERE Title=? COLLATE NOCASE', (title,))
db.action('DELETE from pastissues WHERE BookID=? COLLATE NOCASE', (title,))
db.action('DELETE from wanted where BookID=? COLLATE NOCASE', (title,))
logger.info(f'Magazine {title} removed from database')
passed += 1
elif action == "Reset":
control_value_dict = {"Title": title}
new_value_dict = {
"LastAcquired": '',
"IssueDate": '',
"LatestCover": '',
"IssueStatus": "Wanted"
}
db.upsert("magazines", new_value_dict, control_value_dict)
logger.info(f'Magazine {title} details reset')
passed += 1
elif action == 'Subscribe':
cookie = cherrypy.request.cookie
if cookie and 'll_uid' in list(cookie.keys()):
userid = cookie['ll_uid'].value
res = db.match("SELECT * from subscribers WHERE UserID=? and Type=? and WantID=?",
(userid, 'magazine', title))
if res:
logger.debug(f"User {userid} is already subscribed to {title}")
failed += 1
else:
db.action('INSERT into subscribers (UserID, Type, WantID) VALUES (?, ?, ?)',
(userid, 'magazine', title))
logger.debug(f"Subscribe {userid} to magazine {title}")
passed += 1
elif action == 'Unsubscribe':
cookie = cherrypy.request.cookie
if cookie and 'll_uid' in list(cookie.keys()):
userid = cookie['ll_uid'].value
db.action('DELETE from subscribers WHERE UserID=? and Type=? and WantID=?',
(userid, 'magazine', title))
res = db.select('SELECT issueid from issues where title=?', (title, ))
for iss in res:
db.action('DELETE from subscribers WHERE UserID=? and Type=? and WantID=?',
(userid, 'magazine', iss['issueid']))
logger.debug(f"Unsubscribe {userid} to magazine {title}")
passed += 1
finally:
db.close()
logger.debug(f"{action.title()}: Pass {passed}, Fail {failed}")
# Return JSON response instead of redirect
total = passed + failed
summary = f"Operation '{action}' completed."
if total > 0:
summary += f" {passed} successful"
if failed > 0:
summary += f", {failed} failed"
summary += f" out of {total} total items."
else:
summary += " No items were processed."
return {
'success': True,
'action': action,
'passed': passed,
'failed': failed,
'total': total,
'summary': summary
}
@cherrypy.expose
def search_for_mag(self, bookid=None):
self.check_permitted(lazylibrarian.perm_search)