mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 18:54:09 -04:00
...
This commit is contained in:
commit
4e940f330d
@ -171,6 +171,13 @@ class TagCategories(QDialog, Ui_TagCategories):
|
|||||||
cat_name = unicode(self.input_box.text()).strip()
|
cat_name = unicode(self.input_box.text()).strip()
|
||||||
if cat_name == '':
|
if cat_name == '':
|
||||||
return False
|
return False
|
||||||
|
comps = [c.strip() for c in cat_name.split('.') if c.strip()]
|
||||||
|
if len(comps) == 0 or '.'.join(comps) != cat_name:
|
||||||
|
error_dialog(self, _('Invalid name'),
|
||||||
|
_('That name contains leading or trailing periods, '
|
||||||
|
'multiple periods in a row or spaces before '
|
||||||
|
'or after periods.')).exec_()
|
||||||
|
return False
|
||||||
for c in self.categories:
|
for c in self.categories:
|
||||||
if strcmp(c, cat_name) == 0:
|
if strcmp(c, cat_name) == 0:
|
||||||
error_dialog(self, _('Name already used'),
|
error_dialog(self, _('Name already used'),
|
||||||
@ -193,6 +200,14 @@ class TagCategories(QDialog, Ui_TagCategories):
|
|||||||
return False
|
return False
|
||||||
if not self.current_cat_name:
|
if not self.current_cat_name:
|
||||||
return False
|
return False
|
||||||
|
comps = [c.strip() for c in cat_name.split('.') if c.strip()]
|
||||||
|
if len(comps) == 0 or '.'.join(comps) != cat_name:
|
||||||
|
error_dialog(self, _('Invalid name'),
|
||||||
|
_('That name contains leading or trailing periods, '
|
||||||
|
'multiple periods in a row or spaces before '
|
||||||
|
'or after periods.')).exec_()
|
||||||
|
return False
|
||||||
|
|
||||||
for c in self.categories:
|
for c in self.categories:
|
||||||
if strcmp(c, cat_name) == 0:
|
if strcmp(c, cat_name) == 0:
|
||||||
error_dialog(self, _('Name already used'),
|
error_dialog(self, _('Name already used'),
|
||||||
|
@ -9,7 +9,7 @@ Browsing book collection by tags.
|
|||||||
|
|
||||||
import traceback, copy, cPickle
|
import traceback, copy, cPickle
|
||||||
|
|
||||||
from itertools import izip
|
from itertools import izip, repeat
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \
|
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \
|
||||||
@ -22,7 +22,7 @@ from calibre.ebooks.metadata import title_sort
|
|||||||
from calibre.gui2 import config, NONE, gprefs
|
from calibre.gui2 import config, NONE, gprefs
|
||||||
from calibre.library.field_metadata import TagsIcons, category_icon_map
|
from calibre.library.field_metadata import TagsIcons, category_icon_map
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
from calibre.utils.icu import sort_key, upper, lower, strcmp
|
from calibre.utils.icu import sort_key, lower, strcmp
|
||||||
from calibre.utils.search_query_parser import saved_searches
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
from calibre.utils.formatter import eval_formatter
|
from calibre.utils.formatter import eval_formatter
|
||||||
from calibre.gui2 import error_dialog, question_dialog
|
from calibre.gui2 import error_dialog, question_dialog
|
||||||
@ -32,6 +32,9 @@ from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
|||||||
from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog
|
from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog
|
||||||
from calibre.gui2.widgets import HistoryLineEdit
|
from calibre.gui2.widgets import HistoryLineEdit
|
||||||
|
|
||||||
|
def original_name(t):
|
||||||
|
return getattr(t, 'original_name', t.name)
|
||||||
|
|
||||||
class TagDelegate(QItemDelegate): # {{{
|
class TagDelegate(QItemDelegate): # {{{
|
||||||
|
|
||||||
def paint(self, painter, option, index):
|
def paint(self, painter, option, index):
|
||||||
@ -228,9 +231,13 @@ class TagsView(QTreeView): # {{{
|
|||||||
self._toggle(index, set_to=search_state)
|
self._toggle(index, set_to=search_state)
|
||||||
return
|
return
|
||||||
if action == 'add_to_category':
|
if action == 'add_to_category':
|
||||||
self.add_item_to_user_cat.emit(category,
|
tag = index.tag
|
||||||
getattr(index, 'original_name', index.name),
|
if len(index.children) > 0:
|
||||||
index.category)
|
for c in index.children:
|
||||||
|
self.add_item_to_user_cat.emit(category, original_name(c.tag),
|
||||||
|
c.tag.category)
|
||||||
|
self.add_item_to_user_cat.emit(category, original_name(tag),
|
||||||
|
tag.category)
|
||||||
return
|
return
|
||||||
if action == 'add_subcategory':
|
if action == 'add_subcategory':
|
||||||
self.add_subcategory.emit(key)
|
self.add_subcategory.emit(key)
|
||||||
@ -242,8 +249,12 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.delete_user_category.emit(key)
|
self.delete_user_category.emit(key)
|
||||||
return
|
return
|
||||||
if action == 'delete_item_from_user_category':
|
if action == 'delete_item_from_user_category':
|
||||||
self.del_item_from_user_cat.emit(key,
|
tag = index.tag
|
||||||
getattr(index, 'original_name', index.name), index.category)
|
if len(index.children) > 0:
|
||||||
|
for c in index.children:
|
||||||
|
self.del_item_from_user_cat.emit(key, original_name(c.tag),
|
||||||
|
c.tag.category)
|
||||||
|
self.del_item_from_user_cat.emit(key, original_name(tag), tag.category)
|
||||||
return
|
return
|
||||||
if action == 'manage_searches':
|
if action == 'manage_searches':
|
||||||
self.saved_search_edit.emit(category)
|
self.saved_search_edit.emit(category)
|
||||||
@ -278,8 +289,8 @@ class TagsView(QTreeView): # {{{
|
|||||||
tag = None
|
tag = None
|
||||||
|
|
||||||
if item.type == TagTreeItem.TAG:
|
if item.type == TagTreeItem.TAG:
|
||||||
|
tag_item = item
|
||||||
tag = item.tag
|
tag = item.tag
|
||||||
can_edit = getattr(tag, 'can_edit', True)
|
|
||||||
while item.type != TagTreeItem.CATEGORY:
|
while item.type != TagTreeItem.CATEGORY:
|
||||||
item = item.parent
|
item = item.parent
|
||||||
|
|
||||||
@ -297,7 +308,7 @@ class TagsView(QTreeView): # {{{
|
|||||||
if tag:
|
if tag:
|
||||||
# If the user right-clicked on an editable item, then offer
|
# If the user right-clicked on an editable item, then offer
|
||||||
# the possibility of renaming that item.
|
# the possibility of renaming that item.
|
||||||
if can_edit:
|
if tag.is_editable:
|
||||||
# Add the 'rename' items
|
# Add the 'rename' items
|
||||||
self.context_menu.addAction(_('Rename %s')%tag.name,
|
self.context_menu.addAction(_('Rename %s')%tag.name,
|
||||||
partial(self.context_menu_handler, action='edit_item',
|
partial(self.context_menu_handler, action='edit_item',
|
||||||
@ -317,8 +328,7 @@ class TagsView(QTreeView): # {{{
|
|||||||
m.addAction(self.user_category_icon, n,
|
m.addAction(self.user_category_icon, n,
|
||||||
partial(self.context_menu_handler,
|
partial(self.context_menu_handler,
|
||||||
'add_to_category',
|
'add_to_category',
|
||||||
category='.'.join(p),
|
category='.'.join(p), index=tag_item))
|
||||||
index=tag))
|
|
||||||
if len(tree_dict[k]):
|
if len(tree_dict[k]):
|
||||||
tm = m.addMenu(self.user_category_icon,
|
tm = m.addMenu(self.user_category_icon,
|
||||||
_('Children of %s')%n)
|
_('Children of %s')%n)
|
||||||
@ -331,7 +341,7 @@ class TagsView(QTreeView): # {{{
|
|||||||
_('Remove %s from category %s')%(tag.name, item.py_name),
|
_('Remove %s from category %s')%(tag.name, item.py_name),
|
||||||
partial(self.context_menu_handler,
|
partial(self.context_menu_handler,
|
||||||
action='delete_item_from_user_category',
|
action='delete_item_from_user_category',
|
||||||
key = key, index = tag))
|
key = key, index = tag_item))
|
||||||
# Add the search for value items
|
# Add the search for value items
|
||||||
self.context_menu.addAction(self.search_icon,
|
self.context_menu.addAction(self.search_icon,
|
||||||
_('Search for %s')%tag.name,
|
_('Search for %s')%tag.name,
|
||||||
@ -345,7 +355,7 @@ class TagsView(QTreeView): # {{{
|
|||||||
index=index))
|
index=index))
|
||||||
self.context_menu.addSeparator()
|
self.context_menu.addSeparator()
|
||||||
elif key.startswith('@') and not item.is_gst:
|
elif key.startswith('@') and not item.is_gst:
|
||||||
if item.can_edit:
|
if item.can_be_edited:
|
||||||
self.context_menu.addAction(self.user_category_icon,
|
self.context_menu.addAction(self.user_category_icon,
|
||||||
_('Rename %s')%item.py_name,
|
_('Rename %s')%item.py_name,
|
||||||
partial(self.context_menu_handler, action='edit_item',
|
partial(self.context_menu_handler, action='edit_item',
|
||||||
@ -386,8 +396,8 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.db.field_metadata[key]['is_custom']:
|
self.db.field_metadata[key]['is_custom']:
|
||||||
self.context_menu.addAction(_('Manage %s')%category,
|
self.context_menu.addAction(_('Manage %s')%category,
|
||||||
partial(self.context_menu_handler, action='open_editor',
|
partial(self.context_menu_handler, action='open_editor',
|
||||||
category=getattr(tag, 'original_name', tag.name)
|
category=original_name(tag) if tag else None,
|
||||||
if tag else None, key=key))
|
key=key))
|
||||||
elif key == 'authors':
|
elif key == 'authors':
|
||||||
self.context_menu.addAction(_('Manage %s')%category,
|
self.context_menu.addAction(_('Manage %s')%category,
|
||||||
partial(self.context_menu_handler, action='edit_author_sort'))
|
partial(self.context_menu_handler, action='edit_author_sort'))
|
||||||
@ -524,9 +534,11 @@ class TagTreeItem(object): # {{{
|
|||||||
ROOT = 2
|
ROOT = 2
|
||||||
|
|
||||||
def __init__(self, data=None, category_icon=None, icon_map=None,
|
def __init__(self, data=None, category_icon=None, icon_map=None,
|
||||||
parent=None, tooltip=None, category_key=None):
|
parent=None, tooltip=None, category_key=None, temporary=False):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.children = []
|
self.children = []
|
||||||
|
self.id_set = set()
|
||||||
|
self.is_gst = False
|
||||||
self.boxed = False
|
self.boxed = False
|
||||||
if self.parent is not None:
|
if self.parent is not None:
|
||||||
self.parent.append(self)
|
self.parent.append(self)
|
||||||
@ -541,6 +553,7 @@ class TagTreeItem(object): # {{{
|
|||||||
self.bold_font.setBold(True)
|
self.bold_font.setBold(True)
|
||||||
self.bold_font = QVariant(self.bold_font)
|
self.bold_font = QVariant(self.bold_font)
|
||||||
self.category_key = category_key
|
self.category_key = category_key
|
||||||
|
self.temporary = temporary
|
||||||
elif self.type == self.TAG:
|
elif self.type == self.TAG:
|
||||||
icon_map[0] = data.icon
|
icon_map[0] = data.icon
|
||||||
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
|
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
|
||||||
@ -597,18 +610,20 @@ class TagTreeItem(object): # {{{
|
|||||||
p = self
|
p = self
|
||||||
while p.parent.type != self.ROOT:
|
while p.parent.type != self.ROOT:
|
||||||
p = p.parent
|
p = p.parent
|
||||||
if p.category_key.startswith('@'):
|
if not tag.is_hierarchical:
|
||||||
name = getattr(tag, 'original_name', tag.name)
|
name = original_name(tag)
|
||||||
else:
|
else:
|
||||||
name = tag.name
|
name = tag.name
|
||||||
tt_author = False
|
tt_author = False
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
if tag.count == 0:
|
count = len(self.id_set)
|
||||||
|
count = count if count > 0 else tag.count
|
||||||
|
if count == 0:
|
||||||
return QVariant('%s'%(name))
|
return QVariant('%s'%(name))
|
||||||
else:
|
else:
|
||||||
return QVariant('[%d] %s'%(tag.count, name))
|
return QVariant('[%d] %s'%(count, name))
|
||||||
if role == Qt.EditRole:
|
if role == Qt.EditRole:
|
||||||
return QVariant(getattr(tag, 'original_name', tag.name))
|
return QVariant(original_name(tag))
|
||||||
if role == Qt.DecorationRole:
|
if role == Qt.DecorationRole:
|
||||||
return self.icon_state_map[tag.state]
|
return self.icon_state_map[tag.state]
|
||||||
if role == Qt.ToolTipRole:
|
if role == Qt.ToolTipRole:
|
||||||
@ -671,7 +686,9 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
self.filter_categories_by = filter_categories_by
|
self.filter_categories_by = filter_categories_by
|
||||||
self.collapse_model = collapse_model
|
self.collapse_model = collapse_model
|
||||||
|
|
||||||
# get_node_tree cannot return None here, because row_map is empty
|
# get_node_tree cannot return None here, because row_map is empty. Note
|
||||||
|
# that get_node_tree can indirectly change the user_categories dict.
|
||||||
|
|
||||||
data = self.get_node_tree(config['sort_tags_by'])
|
data = self.get_node_tree(config['sort_tags_by'])
|
||||||
gst = db.prefs.get('grouped_search_terms', {})
|
gst = db.prefs.get('grouped_search_terms', {})
|
||||||
self.root_item = TagTreeItem()
|
self.root_item = TagTreeItem()
|
||||||
@ -693,7 +710,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
tt = _(u'The lookup/search name is "{0}"').format(r)
|
tt = _(u'The lookup/search name is "{0}"').format(r)
|
||||||
|
|
||||||
if r.startswith('@'):
|
if r.startswith('@'):
|
||||||
path_parts = [p.strip() for p in r.split('.') if p.strip()]
|
path_parts = [p for p in r.split('.')]
|
||||||
path = ''
|
path = ''
|
||||||
last_category_node = self.root_item
|
last_category_node = self.root_item
|
||||||
tree_root = self.category_node_tree
|
tree_root = self.category_node_tree
|
||||||
@ -708,7 +725,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
last_category_node = node
|
last_category_node = node
|
||||||
category_node_map[path] = node
|
category_node_map[path] = node
|
||||||
self.category_nodes.append(node)
|
self.category_nodes.append(node)
|
||||||
node.can_edit = (not is_gst) and (i == (len(path_parts)-1))
|
node.can_be_edited = (not is_gst) and (i == (len(path_parts)-1))
|
||||||
node.is_gst = is_gst
|
node.is_gst = is_gst
|
||||||
if not is_gst:
|
if not is_gst:
|
||||||
tree_root[p] = {}
|
tree_root[p] = {}
|
||||||
@ -741,6 +758,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
if idx.isValid():
|
if idx.isValid():
|
||||||
# get some useful serializable data
|
# get some useful serializable data
|
||||||
node = idx.internalPointer()
|
node = idx.internalPointer()
|
||||||
|
path = self.path_for_index(idx)
|
||||||
if node.type == TagTreeItem.CATEGORY:
|
if node.type == TagTreeItem.CATEGORY:
|
||||||
d = (node.type, node.py_name, node.category_key)
|
d = (node.type, node.py_name, node.category_key)
|
||||||
else:
|
else:
|
||||||
@ -748,8 +766,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
p = node
|
p = node
|
||||||
while p.type != TagTreeItem.CATEGORY:
|
while p.type != TagTreeItem.CATEGORY:
|
||||||
p = p.parent
|
p = p.parent
|
||||||
d = (node.type, p.category_key, p.is_gst,
|
d = (node.type, p.category_key, p.is_gst, original_name(t),
|
||||||
getattr(t, 'original_name', t.name), t.category, t.id)
|
t.category, path)
|
||||||
data.append(d)
|
data.append(d)
|
||||||
else:
|
else:
|
||||||
data.append(None)
|
data.append(None)
|
||||||
@ -788,37 +806,30 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
'''
|
'''
|
||||||
src is a list of tuples representing items to copy. The tuple is
|
src is a list of tuples representing items to copy. The tuple is
|
||||||
(type, containing category key, category key is global search term,
|
(type, containing category key, category key is global search term,
|
||||||
full name, category key, id)
|
full name, category key, path to node)
|
||||||
The 'id' member is ignored, and can be None.
|
|
||||||
The type must be TagTreeItem.TAG
|
The type must be TagTreeItem.TAG
|
||||||
dest is the TagTreeItem node to receive the items
|
dest is the TagTreeItem node to receive the items
|
||||||
action is Qt.CopyAction or Qt.MoveAction
|
action is Qt.CopyAction or Qt.MoveAction
|
||||||
'''
|
'''
|
||||||
user_cats = self.db.prefs.get('user_categories', {})
|
def process_source_node(user_cats, src_parent, src_parent_is_gst,
|
||||||
parent_node = None
|
is_uc, dest_key, node):
|
||||||
copied_node = None
|
'''
|
||||||
for s in src:
|
Copy/move an item and all its children to the destination
|
||||||
src_parent, src_parent_is_gst, src_name, src_cat = s[1:5]
|
'''
|
||||||
parent_node = src_parent
|
copied = False
|
||||||
if src_parent.startswith('@'):
|
src_name = original_name(node.tag)
|
||||||
is_uc = True
|
src_cat = node.tag.category
|
||||||
src_parent = src_parent[1:]
|
|
||||||
else:
|
|
||||||
is_uc = False
|
|
||||||
dest_key = dest.category_key[1:]
|
|
||||||
if dest_key not in user_cats:
|
|
||||||
continue
|
|
||||||
new_cat = []
|
|
||||||
# delete the item if the source is a user category and action is move
|
# delete the item if the source is a user category and action is move
|
||||||
if is_uc and not src_parent_is_gst and src_parent in user_cats and \
|
if is_uc and not src_parent_is_gst and src_parent in user_cats and \
|
||||||
action == Qt.MoveAction:
|
action == Qt.MoveAction:
|
||||||
|
new_cat = []
|
||||||
for tup in user_cats[src_parent]:
|
for tup in user_cats[src_parent]:
|
||||||
if src_name == tup[0] and src_cat == tup[1]:
|
if src_name == tup[0] and src_cat == tup[1]:
|
||||||
continue
|
continue
|
||||||
new_cat.append(list(tup))
|
new_cat.append(list(tup))
|
||||||
user_cats[src_parent] = new_cat
|
user_cats[src_parent] = new_cat
|
||||||
else:
|
else:
|
||||||
copied_node = (src_parent, src_name)
|
copied = True
|
||||||
|
|
||||||
# Now add the item to the destination user category
|
# Now add the item to the destination user category
|
||||||
add_it = True
|
add_it = True
|
||||||
@ -830,19 +841,54 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
if add_it:
|
if add_it:
|
||||||
user_cats[dest_key].append([src_name, src_cat, 0])
|
user_cats[dest_key].append([src_name, src_cat, 0])
|
||||||
|
|
||||||
|
for c in node.children:
|
||||||
|
copied = process_source_node(user_cats, src_parent, src_parent_is_gst,
|
||||||
|
is_uc, dest_key, c)
|
||||||
|
return copied
|
||||||
|
|
||||||
|
user_cats = self.db.prefs.get('user_categories', {})
|
||||||
|
parent_node = None
|
||||||
|
copied = False
|
||||||
|
path = None
|
||||||
|
for s in src:
|
||||||
|
src_parent, src_parent_is_gst = s[1:3]
|
||||||
|
path = s[5]
|
||||||
|
parent_node = src_parent
|
||||||
|
|
||||||
|
if src_parent.startswith('@'):
|
||||||
|
is_uc = True
|
||||||
|
src_parent = src_parent[1:]
|
||||||
|
else:
|
||||||
|
is_uc = False
|
||||||
|
dest_key = dest.category_key[1:]
|
||||||
|
|
||||||
|
if dest_key not in user_cats:
|
||||||
|
continue
|
||||||
|
|
||||||
|
node = self.index_for_path(path)
|
||||||
|
if node:
|
||||||
|
copied = process_source_node(user_cats, src_parent, src_parent_is_gst,
|
||||||
|
is_uc, dest_key, node.internalPointer())
|
||||||
|
|
||||||
self.db.prefs.set('user_categories', user_cats)
|
self.db.prefs.set('user_categories', user_cats)
|
||||||
self.tags_view.recount()
|
self.tags_view.recount()
|
||||||
|
|
||||||
|
# Scroll to the item copied. If it was moved, scroll to the parent
|
||||||
if parent_node is not None:
|
if parent_node is not None:
|
||||||
|
self.clear_boxed()
|
||||||
m = self.tags_view.model()
|
m = self.tags_view.model()
|
||||||
if copied_node is not None:
|
if not copied:
|
||||||
path = m.find_item_node(parent_node, copied_node[1], None,
|
p = path[-1]
|
||||||
equals_match=True)
|
if p == 0:
|
||||||
else:
|
path = m.find_category_node(parent_node)
|
||||||
path = m.find_category_node(parent_node)
|
else:
|
||||||
|
path[-1] = p - 1
|
||||||
idx = m.index_for_path(path)
|
idx = m.index_for_path(path)
|
||||||
self.tags_view.setExpanded(idx, True)
|
self.tags_view.setExpanded(idx, True)
|
||||||
m.show_item_at_index(idx)
|
if idx.internalPointer().type == TagTreeItem.TAG:
|
||||||
|
m.show_item_at_index(idx, box=True)
|
||||||
|
else:
|
||||||
|
m.show_item_at_index(idx)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def do_drop_from_library(self, md, action, row, column, parent):
|
def do_drop_from_library(self, md, action, row, column, parent):
|
||||||
@ -1044,24 +1090,60 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
else:
|
else:
|
||||||
collapse_model = 'partition'
|
collapse_model = 'partition'
|
||||||
collapse_template = tweaks['categories_collapsed_popularity_template']
|
collapse_template = tweaks['categories_collapsed_popularity_template']
|
||||||
collapse_letter = collapse_letter_sk = None
|
|
||||||
|
|
||||||
def process_one_node(category, state_map, collapse_letter, collapse_letter_sk):
|
def process_one_node(category, state_map):
|
||||||
|
collapse_letter = None
|
||||||
category_index = self.createIndex(category.row(), 0, category)
|
category_index = self.createIndex(category.row(), 0, category)
|
||||||
category_node = category_index.internalPointer()
|
category_node = category_index.internalPointer()
|
||||||
key = category_node.category_key
|
key = category_node.category_key
|
||||||
if key not in data:
|
if key not in data:
|
||||||
return ((collapse_letter, collapse_letter_sk))
|
return
|
||||||
cat_len = len(data[key])
|
cat_len = len(data[key])
|
||||||
if cat_len <= 0:
|
if cat_len <= 0:
|
||||||
return ((collapse_letter, collapse_letter_sk))
|
return
|
||||||
|
|
||||||
|
category_child_map = {}
|
||||||
fm = self.db.field_metadata[key]
|
fm = self.db.field_metadata[key]
|
||||||
clear_rating = True if key not in self.categories_with_ratings and \
|
clear_rating = True if key not in self.categories_with_ratings and \
|
||||||
not fm['is_custom'] and \
|
not fm['is_custom'] and \
|
||||||
not fm['kind'] == 'user' \
|
not fm['kind'] == 'user' \
|
||||||
else False
|
else False
|
||||||
tt = key if fm['kind'] == 'user' else None
|
tt = key if fm['kind'] == 'user' else None
|
||||||
|
|
||||||
|
if collapse_model == 'first letter':
|
||||||
|
# Build a list of 'equal' first letters by looking for
|
||||||
|
# overlapping ranges. If a range overlaps another, then the
|
||||||
|
# letters are assumed to be equivalent. ICU collating is complex
|
||||||
|
# beyond belief. This mechanism lets us determine the logical
|
||||||
|
# first character from ICU's standpoint.
|
||||||
|
chardict = {}
|
||||||
|
for idx,tag in enumerate(data[key]):
|
||||||
|
if not tag.sort:
|
||||||
|
c = ' '
|
||||||
|
else:
|
||||||
|
c = icu_upper(tag.sort[0])
|
||||||
|
if c not in chardict:
|
||||||
|
chardict[c] = [idx, idx]
|
||||||
|
else:
|
||||||
|
chardict[c][1] = idx
|
||||||
|
|
||||||
|
# sort the ranges to facilitate detecting overlap
|
||||||
|
ranges = sorted([(v[0], v[1], c) for c,v in chardict.items()])
|
||||||
|
|
||||||
|
# Create a list of 'first letters' to use for each item in
|
||||||
|
# the category. The list is generated using the ranges. Overlaps
|
||||||
|
# are filled with the character that first occurs.
|
||||||
|
cl_list = list(repeat(None, len(data[key])))
|
||||||
|
for t in ranges:
|
||||||
|
start = t[0]
|
||||||
|
c = t[2]
|
||||||
|
if cl_list[start] is None:
|
||||||
|
nc = c
|
||||||
|
else:
|
||||||
|
nc = cl_list[start]
|
||||||
|
for i in range(start, t[1]+1):
|
||||||
|
cl_list[i] = nc
|
||||||
|
|
||||||
for idx,tag in enumerate(data[key]):
|
for idx,tag in enumerate(data[key]):
|
||||||
if clear_rating:
|
if clear_rating:
|
||||||
tag.avg_rating = None
|
tag.avg_rating = None
|
||||||
@ -1078,72 +1160,77 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
name = eval_formatter.safe_format(collapse_template,
|
name = eval_formatter.safe_format(collapse_template,
|
||||||
d, 'TAG_VIEW', None)
|
d, 'TAG_VIEW', None)
|
||||||
self.beginInsertRows(category_index, 999999, 1) #len(data[key])-1)
|
self.beginInsertRows(category_index, 999999, 1) #len(data[key])-1)
|
||||||
sub_cat = TagTreeItem(parent=category,
|
sub_cat = TagTreeItem(parent=category, data = name,
|
||||||
data = name, tooltip = None,
|
tooltip = None, temporary=True,
|
||||||
category_icon = category_node.icon,
|
category_icon = category_node.icon,
|
||||||
category_key=category_node.category_key)
|
category_key=category_node.category_key)
|
||||||
self.endInsertRows()
|
self.endInsertRows()
|
||||||
else:
|
else: # by 'first letter'
|
||||||
ts = tag.sort
|
cl = cl_list[idx]
|
||||||
if not ts:
|
if cl != collapse_letter:
|
||||||
ts = ' '
|
collapse_letter = cl
|
||||||
try:
|
|
||||||
sk = sort_key(ts)[0]
|
|
||||||
except:
|
|
||||||
sk = ts[0]
|
|
||||||
|
|
||||||
if sk != collapse_letter_sk:
|
|
||||||
collapse_letter = upper(ts[0])
|
|
||||||
try:
|
|
||||||
collapse_letter_sk = sort_key(collapse_letter)[0]
|
|
||||||
except:
|
|
||||||
collapse_letter_sk = collapse_letter
|
|
||||||
sub_cat = TagTreeItem(parent=category,
|
sub_cat = TagTreeItem(parent=category,
|
||||||
data = collapse_letter,
|
data = collapse_letter,
|
||||||
category_icon = category_node.icon,
|
category_icon = category_node.icon,
|
||||||
tooltip = None,
|
tooltip = None, temporary=True,
|
||||||
category_key=category_node.category_key)
|
category_key=category_node.category_key)
|
||||||
node_parent = sub_cat
|
node_parent = sub_cat
|
||||||
else:
|
else:
|
||||||
node_parent = category
|
node_parent = category
|
||||||
|
|
||||||
components = [t for t in tag.name.split('.')]
|
# category display order is important here. The following works
|
||||||
if key in ['authors', 'publisher', 'news', 'formats', 'rating'] or \
|
# only of all the non-user categories are displayed before the
|
||||||
key not in self.db.prefs.get('categories_using_hierarchy', []) or\
|
# user categories
|
||||||
len(components) == 1 or \
|
components = [t.strip() for t in original_name(tag).split('.')
|
||||||
fm['kind'] == 'user':
|
if t.strip()]
|
||||||
|
if len(components) == 0 or '.'.join(components) != original_name(tag):
|
||||||
|
components = [original_name(tag)]
|
||||||
|
in_uc = fm['kind'] == 'user'
|
||||||
|
if (not tag.is_hierarchical) and (in_uc or
|
||||||
|
key in ['authors', 'publisher', 'news', 'formats', 'rating'] or
|
||||||
|
key not in self.db.prefs.get('categories_using_hierarchy', []) or
|
||||||
|
len(components) == 1):
|
||||||
self.beginInsertRows(category_index, 999999, 1)
|
self.beginInsertRows(category_index, 999999, 1)
|
||||||
TagTreeItem(parent=node_parent, data=tag, tooltip=tt,
|
n = TagTreeItem(parent=node_parent, data=tag, tooltip=tt,
|
||||||
icon_map=self.icon_state_map)
|
icon_map=self.icon_state_map)
|
||||||
|
if tag.id_set is not None:
|
||||||
|
n.id_set |= tag.id_set
|
||||||
|
category_child_map[tag.name, tag.category] = n
|
||||||
self.endInsertRows()
|
self.endInsertRows()
|
||||||
tag.can_edit = key != 'formats' and (key == 'news' or \
|
tag.is_editable = key != 'formats' and (key == 'news' or \
|
||||||
self.db.field_metadata[tag.category]['datatype'] in \
|
self.db.field_metadata[tag.category]['datatype'] in \
|
||||||
['text', 'series', 'enumeration'])
|
['text', 'series', 'enumeration'])
|
||||||
else:
|
else:
|
||||||
for i,comp in enumerate(components):
|
for i,comp in enumerate(components):
|
||||||
child_map = dict([(t.tag.name, t) for t in node_parent.children
|
if i == 0:
|
||||||
|
child_map = category_child_map
|
||||||
|
else:
|
||||||
|
child_map = dict([((t.tag.name, t.tag.category), t)
|
||||||
|
for t in node_parent.children
|
||||||
if t.type != TagTreeItem.CATEGORY])
|
if t.type != TagTreeItem.CATEGORY])
|
||||||
if comp in child_map:
|
if (comp,tag.category) in child_map:
|
||||||
node_parent = child_map[comp]
|
node_parent = child_map[(comp,tag.category)]
|
||||||
node_parent.tag.count += tag.count
|
node_parent.tag.is_hierarchical = True
|
||||||
node_parent.tag.use_prefix = True
|
|
||||||
else:
|
else:
|
||||||
if i < len(components)-1:
|
if i < len(components)-1:
|
||||||
t = copy.copy(tag)
|
t = copy.copy(tag)
|
||||||
t.original_name = '.'.join(components[:i+1])
|
t.original_name = '.'.join(components[:i+1])
|
||||||
t.can_edit = False
|
t.is_editable = False
|
||||||
else:
|
else:
|
||||||
t = tag
|
t = tag
|
||||||
t.original_name = t.name
|
if not in_uc:
|
||||||
t.can_edit = True
|
t.original_name = t.name
|
||||||
t.use_prefix = True
|
t.is_editable = True
|
||||||
|
t.is_hierarchical = True
|
||||||
t.name = comp
|
t.name = comp
|
||||||
self.beginInsertRows(category_index, 999999, 1)
|
self.beginInsertRows(category_index, 999999, 1)
|
||||||
node_parent = TagTreeItem(parent=node_parent, data=t,
|
node_parent = TagTreeItem(parent=node_parent, data=t,
|
||||||
tooltip=tt, icon_map=self.icon_state_map)
|
tooltip=tt, icon_map=self.icon_state_map)
|
||||||
|
child_map[(comp,tag.category)] = node_parent
|
||||||
self.endInsertRows()
|
self.endInsertRows()
|
||||||
|
# This id_set must not be None
|
||||||
return ((collapse_letter, collapse_letter_sk))
|
node_parent.id_set |= tag.id_set
|
||||||
|
return
|
||||||
|
|
||||||
for category in self.category_nodes:
|
for category in self.category_nodes:
|
||||||
if len(category.children) > 0:
|
if len(category.children) > 0:
|
||||||
@ -1151,7 +1238,11 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
states = [c.tag.state for c in category.child_tags()]
|
states = [c.tag.state for c in category.child_tags()]
|
||||||
names = [(c.tag.name, c.tag.category) for c in category.child_tags()]
|
names = [(c.tag.name, c.tag.category) for c in category.child_tags()]
|
||||||
state_map = dict(izip(names, states))
|
state_map = dict(izip(names, states))
|
||||||
ctags = [c for c in child_map if c.type == TagTreeItem.CATEGORY]
|
# temporary sub-categories (the partitioning ones) must follow
|
||||||
|
# the permanent sub-categories. This will happen naturally if
|
||||||
|
# the temp ones are added by process_node
|
||||||
|
ctags = [c for c in child_map if
|
||||||
|
c.type == TagTreeItem.CATEGORY and not c.temporary]
|
||||||
start = len(ctags)
|
start = len(ctags)
|
||||||
self.beginRemoveRows(self.createIndex(category.row(), 0, category),
|
self.beginRemoveRows(self.createIndex(category.row(), 0, category),
|
||||||
start, len(child_map)-1)
|
start, len(child_map)-1)
|
||||||
@ -1160,8 +1251,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
else:
|
else:
|
||||||
state_map = {}
|
state_map = {}
|
||||||
|
|
||||||
collapse_letter, collapse_letter_sk = process_one_node(category,
|
process_one_node(category, state_map)
|
||||||
state_map, collapse_letter, collapse_letter_sk)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def columnCount(self, parent):
|
def columnCount(self, parent):
|
||||||
@ -1180,13 +1270,19 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
# working with the last item and that item is deleted, in which case
|
# working with the last item and that item is deleted, in which case
|
||||||
# we position at the parent label
|
# we position at the parent label
|
||||||
path = index.model().path_for_index(index)
|
path = index.model().path_for_index(index)
|
||||||
val = unicode(value.toString())
|
val = unicode(value.toString()).strip()
|
||||||
if not val:
|
if not val:
|
||||||
error_dialog(self.tags_view, _('Item is blank'),
|
error_dialog(self.tags_view, _('Item is blank'),
|
||||||
_('An item cannot be set to nothing. Delete it instead.')).exec_()
|
_('An item cannot be set to nothing. Delete it instead.')).exec_()
|
||||||
return False
|
return False
|
||||||
item = index.internalPointer()
|
item = index.internalPointer()
|
||||||
if item.type == TagTreeItem.CATEGORY and item.category_key.startswith('@'):
|
if item.type == TagTreeItem.CATEGORY and item.category_key.startswith('@'):
|
||||||
|
if val.find('.') >= 0:
|
||||||
|
error_dialog(self.tags_view, _('Rename user category'),
|
||||||
|
_('You cannot use periods in the name when '
|
||||||
|
'renaming user categories'), show=True)
|
||||||
|
return False
|
||||||
|
|
||||||
user_cats = self.db.prefs.get('user_categories', {})
|
user_cats = self.db.prefs.get('user_categories', {})
|
||||||
ckey = item.category_key[1:]
|
ckey = item.category_key[1:]
|
||||||
dotpos = ckey.rfind('.')
|
dotpos = ckey.rfind('.')
|
||||||
@ -1199,7 +1295,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
if len(c) == len(ckey):
|
if len(c) == len(ckey):
|
||||||
if nkey in user_cats:
|
if nkey in user_cats:
|
||||||
error_dialog(self.tags_view, _('Rename user category'),
|
error_dialog(self.tags_view, _('Rename user category'),
|
||||||
_('The name %s is already used'%nkey), show=True)
|
_('The name %s is already used')%nkey, show=True)
|
||||||
return False
|
return False
|
||||||
user_cats[nkey] = user_cats[ckey]
|
user_cats[nkey] = user_cats[ckey]
|
||||||
del user_cats[ckey]
|
del user_cats[ckey]
|
||||||
@ -1219,7 +1315,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
key = item.tag.category
|
key = item.tag.category
|
||||||
name = getattr(item.tag, 'original_name', item.tag.name)
|
name = original_name(item.tag)
|
||||||
# make certain we know about the item's category
|
# make certain we know about the item's category
|
||||||
if key not in self.db.field_metadata:
|
if key not in self.db.field_metadata:
|
||||||
return False
|
return False
|
||||||
@ -1306,7 +1402,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
if index.isValid():
|
if index.isValid():
|
||||||
node = self.data(index, Qt.UserRole)
|
node = self.data(index, Qt.UserRole)
|
||||||
if node.type == TagTreeItem.TAG:
|
if node.type == TagTreeItem.TAG:
|
||||||
if getattr(node.tag, 'can_edit', True):
|
if node.tag.is_editable:
|
||||||
ans |= Qt.ItemIsDragEnabled
|
ans |= Qt.ItemIsDragEnabled
|
||||||
fm = self.db.metadata_for_field(node.tag.category)
|
fm = self.db.metadata_for_field(node.tag.category)
|
||||||
if node.tag.category in \
|
if node.tag.category in \
|
||||||
@ -1438,8 +1534,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
|
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
|
||||||
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
|
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
|
||||||
else:
|
else:
|
||||||
name = getattr(tag, 'original_name', tag.name)
|
name = original_name(tag)
|
||||||
use_prefix = getattr(tag, 'use_prefix', False)
|
use_prefix = tag.is_hierarchical
|
||||||
if category == 'tags':
|
if category == 'tags':
|
||||||
if name in tags_seen:
|
if name in tags_seen:
|
||||||
continue
|
continue
|
||||||
@ -1477,7 +1573,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
tag = tag_item.tag
|
tag = tag_item.tag
|
||||||
if tag is None:
|
if tag is None:
|
||||||
return False
|
return False
|
||||||
name = getattr(tag, 'original_name', tag.name)
|
name = original_name(tag)
|
||||||
if (equals_match and strcmp(name, txt) == 0) or \
|
if (equals_match and strcmp(name, txt) == 0) or \
|
||||||
(not equals_match and lower(name).find(txt) >= 0):
|
(not equals_match and lower(name).find(txt) >= 0):
|
||||||
self.path_found = path
|
self.path_found = path
|
||||||
@ -1559,11 +1655,16 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
if tag_item.boxed:
|
if tag_item.boxed:
|
||||||
tag_item.boxed = False
|
tag_item.boxed = False
|
||||||
self.dataChanged.emit(tag_index, tag_index)
|
self.dataChanged.emit(tag_index, tag_index)
|
||||||
|
for i,c in enumerate(tag_item.children):
|
||||||
|
process_tag(self.index(i, 0, tag_index), c)
|
||||||
|
|
||||||
def process_level(category_index):
|
def process_level(category_index):
|
||||||
for j in xrange(self.rowCount(category_index)):
|
for j in xrange(self.rowCount(category_index)):
|
||||||
tag_index = self.index(j, 0, category_index)
|
tag_index = self.index(j, 0, category_index)
|
||||||
tag_item = tag_index.internalPointer()
|
tag_item = tag_index.internalPointer()
|
||||||
|
if tag_item.boxed:
|
||||||
|
tag_item.boxed = False
|
||||||
|
self.dataChanged.emit(tag_index, tag_index)
|
||||||
if tag_item.type == TagTreeItem.CATEGORY:
|
if tag_item.type == TagTreeItem.CATEGORY:
|
||||||
process_level(tag_index)
|
process_level(tag_index)
|
||||||
else:
|
else:
|
||||||
@ -1703,8 +1804,9 @@ class TagBrowserMixin(object): # {{{
|
|||||||
db = self.library_view.model().db
|
db = self.library_view.model().db
|
||||||
user_cats = db.prefs.get('user_categories', {})
|
user_cats = db.prefs.get('user_categories', {})
|
||||||
|
|
||||||
if dest_category.startswith('@'):
|
if dest_category and dest_category.startswith('@'):
|
||||||
dest_category = dest_category[1:]
|
dest_category = dest_category[1:]
|
||||||
|
|
||||||
if dest_category not in user_cats:
|
if dest_category not in user_cats:
|
||||||
return error_dialog(self.tags_view, _('Add to user category'),
|
return error_dialog(self.tags_view, _('Add to user category'),
|
||||||
_('A user category %s does not exist')%dest_category, show=True)
|
_('A user category %s does not exist')%dest_category, show=True)
|
||||||
|
@ -46,11 +46,14 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
|||||||
class Tag(object):
|
class Tag(object):
|
||||||
|
|
||||||
def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None,
|
def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None,
|
||||||
tooltip=None, icon=None, category=None):
|
tooltip=None, icon=None, category=None, id_set=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.id = id
|
self.id = id
|
||||||
self.count = count
|
self.count = count
|
||||||
self.state = state
|
self.state = state
|
||||||
|
self.is_hierarchical = False
|
||||||
|
self.is_editable = True
|
||||||
|
self.id_set = id_set
|
||||||
self.avg_rating = avg/2.0 if avg is not None else 0
|
self.avg_rating = avg/2.0 if avg is not None else 0
|
||||||
self.sort = sort
|
self.sort = sort
|
||||||
if self.avg_rating > 0:
|
if self.avg_rating > 0:
|
||||||
@ -1160,6 +1163,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.n = name
|
self.n = name
|
||||||
self.s = sort
|
self.s = sort
|
||||||
self.c = 0
|
self.c = 0
|
||||||
|
self.id_set = set()
|
||||||
self.rt = 0
|
self.rt = 0
|
||||||
self.rc = 0
|
self.rc = 0
|
||||||
self.id = None
|
self.id = None
|
||||||
@ -1177,6 +1181,22 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return 'n=%s s=%s c=%d rt=%d rc=%d id=%s'%\
|
return 'n=%s s=%s c=%d rt=%d rc=%d id=%s'%\
|
||||||
(self.n, self.s, self.c, self.rt, self.rc, self.id)
|
(self.n, self.s, self.c, self.rt, self.rc, self.id)
|
||||||
|
|
||||||
|
def clean_user_categories(self):
|
||||||
|
user_cats = self.prefs.get('user_categories', {})
|
||||||
|
new_cats = {}
|
||||||
|
for k in user_cats:
|
||||||
|
comps = [c.strip() for c in k.split('.') if c.strip()]
|
||||||
|
if len(comps) == 0:
|
||||||
|
i = 1
|
||||||
|
while True:
|
||||||
|
if unicode(i) not in user_cats:
|
||||||
|
new_cats[unicode(i)] = user_cats[k]
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
new_cats['.'.join(comps)] = user_cats[k]
|
||||||
|
self.prefs.set('user_categories', new_cats)
|
||||||
|
return new_cats
|
||||||
|
|
||||||
def get_categories(self, sort='name', ids=None, icon_map=None):
|
def get_categories(self, sort='name', ids=None, icon_map=None):
|
||||||
#start = last = time.clock()
|
#start = last = time.clock()
|
||||||
@ -1264,6 +1284,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
item = tag_class(val, sort_val)
|
item = tag_class(val, sort_val)
|
||||||
tcategories[cat][val] = item
|
tcategories[cat][val] = item
|
||||||
item.c += 1
|
item.c += 1
|
||||||
|
item.id_set.add(book[0])
|
||||||
item.id = item_id
|
item.id = item_id
|
||||||
if rating > 0:
|
if rating > 0:
|
||||||
item.rt += rating
|
item.rt += rating
|
||||||
@ -1281,6 +1302,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
item = tag_class(val, sort_val)
|
item = tag_class(val, sort_val)
|
||||||
tcategories[cat][val] = item
|
tcategories[cat][val] = item
|
||||||
item.c += 1
|
item.c += 1
|
||||||
|
item.id_set.add(book[0])
|
||||||
item.id = item_id
|
item.id = item_id
|
||||||
if rating > 0:
|
if rating > 0:
|
||||||
item.rt += rating
|
item.rt += rating
|
||||||
@ -1368,7 +1390,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
|
|
||||||
categories[category] = [tag_class(formatter(r.n), count=r.c, id=r.id,
|
categories[category] = [tag_class(formatter(r.n), count=r.c, id=r.id,
|
||||||
avg=avgr(r), sort=r.s, icon=icon,
|
avg=avgr(r), sort=r.s, icon=icon,
|
||||||
tooltip=tooltip, category=category)
|
tooltip=tooltip, category=category,
|
||||||
|
id_set=r.id_set)
|
||||||
for r in items]
|
for r in items]
|
||||||
|
|
||||||
#print 'end phase "tags list":', time.clock() - last, 'seconds'
|
#print 'end phase "tags list":', time.clock() - last, 'seconds'
|
||||||
@ -1377,6 +1400,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
# Needed for legacy databases that have multiple ratings that
|
# Needed for legacy databases that have multiple ratings that
|
||||||
# map to n stars
|
# map to n stars
|
||||||
for r in categories['rating']:
|
for r in categories['rating']:
|
||||||
|
r.id_set = None
|
||||||
for x in categories['rating']:
|
for x in categories['rating']:
|
||||||
if r.name == x.name and r.id != x.id:
|
if r.name == x.name and r.id != x.id:
|
||||||
r.count = r.count + x.count
|
r.count = r.count + x.count
|
||||||
@ -1413,7 +1437,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
categories['formats'].sort(key = lambda x:x.name)
|
categories['formats'].sort(key = lambda x:x.name)
|
||||||
|
|
||||||
#### Now do the user-defined categories. ####
|
#### Now do the user-defined categories. ####
|
||||||
user_categories = dict.copy(self.prefs['user_categories'])
|
user_categories = dict.copy(self.clean_user_categories())
|
||||||
|
|
||||||
# We want to use same node in the user category as in the source
|
# We want to use same node in the user category as in the source
|
||||||
# category. To do that, we need to find the original Tag node. There is
|
# category. To do that, we need to find the original Tag node. There is
|
||||||
|
@ -342,6 +342,7 @@ class BrowseServer(object):
|
|||||||
return category_meta[x]['name'].lower()
|
return category_meta[x]['name'].lower()
|
||||||
|
|
||||||
displayed_custom_fields = custom_fields_to_display(self.db)
|
displayed_custom_fields = custom_fields_to_display(self.db)
|
||||||
|
uc_displayed = set()
|
||||||
for category in sorted(categories, key=lambda x: sort_key(getter(x))):
|
for category in sorted(categories, key=lambda x: sort_key(getter(x))):
|
||||||
if len(categories[category]) == 0:
|
if len(categories[category]) == 0:
|
||||||
continue
|
continue
|
||||||
@ -361,7 +362,19 @@ class BrowseServer(object):
|
|||||||
icon = category_icon_map['user:']
|
icon = category_icon_map['user:']
|
||||||
else:
|
else:
|
||||||
icon = 'blank.png'
|
icon = 'blank.png'
|
||||||
cats.append((meta['name'], category, icon))
|
|
||||||
|
if meta['kind'] == 'user':
|
||||||
|
dot = category.find('.')
|
||||||
|
if dot > 0:
|
||||||
|
cat = category[:dot]
|
||||||
|
if cat not in uc_displayed:
|
||||||
|
cats.append((meta['name'][:dot-1], cat, icon))
|
||||||
|
uc_displayed.add(cat)
|
||||||
|
else:
|
||||||
|
cats.append((meta['name'], category, icon))
|
||||||
|
uc_displayed.add(category)
|
||||||
|
else:
|
||||||
|
cats.append((meta['name'], category, icon))
|
||||||
|
|
||||||
cats = [(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}"> </a>'
|
cats = [(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}"> </a>'
|
||||||
u'<img src="{3}{src}" alt="{0}" />'
|
u'<img src="{3}{src}" alt="{0}" />'
|
||||||
@ -394,13 +407,59 @@ class BrowseServer(object):
|
|||||||
category_name = category_meta[category]['name']
|
category_name = category_meta[category]['name']
|
||||||
datatype = category_meta[category]['datatype']
|
datatype = category_meta[category]['datatype']
|
||||||
|
|
||||||
|
# See if we have any sub-categories to display. As we find them, add
|
||||||
|
# them to the displayed set to avoid showing the same item twice
|
||||||
|
uc_displayed = set()
|
||||||
|
cats = []
|
||||||
|
for ucat in sorted(categories.keys(), key=sort_key):
|
||||||
|
if len(categories[ucat]) == 0:
|
||||||
|
continue
|
||||||
|
if category == 'formats':
|
||||||
|
continue
|
||||||
|
meta = category_meta.get(ucat, None)
|
||||||
|
if meta is None:
|
||||||
|
continue
|
||||||
|
if meta['kind'] != 'user':
|
||||||
|
continue
|
||||||
|
cat_len = len(category)
|
||||||
|
if not (len(ucat) > cat_len and ucat.startswith(category+'.')):
|
||||||
|
continue
|
||||||
|
icon = category_icon_map['user:']
|
||||||
|
# we have a subcategory. Find any further dots (further subcats)
|
||||||
|
cat_len += 1
|
||||||
|
cat = ucat[cat_len:]
|
||||||
|
dot = cat.find('.')
|
||||||
|
if dot > 0:
|
||||||
|
# More subcats
|
||||||
|
cat = cat[:dot]
|
||||||
|
if cat not in uc_displayed:
|
||||||
|
cats.append((cat, ucat[:cat_len+dot], icon))
|
||||||
|
uc_displayed.add(cat)
|
||||||
|
else:
|
||||||
|
# This is the end of the chain
|
||||||
|
cats.append((cat, ucat, icon))
|
||||||
|
uc_displayed.add(cat)
|
||||||
|
|
||||||
|
cats = u'\n\n'.join(
|
||||||
|
[(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}"> </a>'
|
||||||
|
u'<img src="{3}{src}" alt="{0}" />'
|
||||||
|
u'<span class="label">{0}</span>'
|
||||||
|
u'</li>')
|
||||||
|
.format(xml(x, True), xml(quote(y)), xml(_('Browse books by')),
|
||||||
|
self.opts.url_prefix, src='/browse/icon/'+z)
|
||||||
|
for x, y, z in cats])
|
||||||
|
if cats:
|
||||||
|
cats = (u'\n<div class="toplevel">\n'
|
||||||
|
'{0}</div>').format(cats)
|
||||||
|
script = 'toplevel();'
|
||||||
|
else:
|
||||||
|
script = 'true'
|
||||||
|
|
||||||
|
# Now do the category items
|
||||||
items = categories[category]
|
items = categories[category]
|
||||||
sort = self.browse_sort_categories(items, sort)
|
sort = self.browse_sort_categories(items, sort)
|
||||||
|
|
||||||
script = 'true'
|
if not cats and len(items) == 1:
|
||||||
|
|
||||||
if len(items) == 1:
|
|
||||||
# Only one item in category, go directly to book list
|
# Only one item in category, go directly to book list
|
||||||
prefix = '' if self.is_wsgi else self.opts.url_prefix
|
prefix = '' if self.is_wsgi else self.opts.url_prefix
|
||||||
html = get_category_items(category, items,
|
html = get_category_items(category, items,
|
||||||
@ -443,7 +502,10 @@ class BrowseServer(object):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
script = 'category(%s);'%script
|
if cats:
|
||||||
|
script = 'toplevel();category(%s);'%script
|
||||||
|
else:
|
||||||
|
script = 'category(%s);'%script
|
||||||
|
|
||||||
main = u'''
|
main = u'''
|
||||||
<div class="category">
|
<div class="category">
|
||||||
@ -453,7 +515,7 @@ class BrowseServer(object):
|
|||||||
{1}
|
{1}
|
||||||
</div>
|
</div>
|
||||||
'''.format(
|
'''.format(
|
||||||
xml(_('Browsing by')+': ' + category_name), items,
|
xml(_('Browsing by')+': ' + category_name), cats + items,
|
||||||
xml(_('Up'), True), self.opts.url_prefix)
|
xml(_('Up'), True), self.opts.url_prefix)
|
||||||
|
|
||||||
return self.browse_template(sort).format(title=category_name,
|
return self.browse_template(sort).format(title=category_name,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user