diff --git a/src/calibre/gui2/dialogs/tag_categories.py b/src/calibre/gui2/dialogs/tag_categories.py index 899d3d1920..49f8fc6457 100644 --- a/src/calibre/gui2/dialogs/tag_categories.py +++ b/src/calibre/gui2/dialogs/tag_categories.py @@ -59,14 +59,24 @@ class TagCategories(QDialog, Ui_TagCategories): ] category_names = ['', _('Authors'), _('Series'), _('Publishers'), _('Tags')] - cc_map = self.db.custom_column_label_map - for cc in cc_map: - if cc_map[cc]['datatype'] in ['text', 'series']: - self.category_labels.append(db.field_metadata.label_to_key(cc)) + cvals = {} + for key,cc in self.db.custom_field_metadata().iteritems(): + if cc['datatype'] in ['text', 'series', 'enumeration']: + self.category_labels.append(key) category_icons.append(cc_icon) - category_values.append(lambda col=cc: self.db.all_custom(label=col)) - category_names.append(cc_map[cc]['name']) - + category_values.append(lambda col=cc['label']: self.db.all_custom(label=col)) + category_names.append(cc['name']) + elif cc['datatype'] == 'composite' and \ + cc['display'].get('make_category', False): + self.category_labels.append(key) + category_icons.append(cc_icon) + category_names.append(cc['name']) + dex = cc['rec_index'] + cvals = set() + for book in db.data.iterall(): + if book[dex]: + cvals.add(book[dex]) + category_values.append(lambda s=list(cvals): s) self.all_items = [] self.all_items_dict = {} for idx,label in enumerate(self.category_labels): @@ -88,7 +98,8 @@ class TagCategories(QDialog, Ui_TagCategories): if l[1] in self.category_labels: if t is None: t = Item(name=l[0], label=l[1], index=len(self.all_items), - icon=category_icons[self.category_labels.index(l[1])], exists=False) + icon=category_icons[self.category_labels.index(l[1])], + exists=False) self.all_items.append(t) self.all_items_dict[key] = t l[2] = t.index @@ -108,13 +119,16 @@ class TagCategories(QDialog, Ui_TagCategories): self.add_category_button.clicked.connect(self.add_category) self.rename_category_button.clicked.connect(self.rename_category) self.category_box.currentIndexChanged[int].connect(self.select_category) - self.category_filter_box.currentIndexChanged[int].connect(self.display_filtered_categories) + self.category_filter_box.currentIndexChanged[int].connect( + self.display_filtered_categories) self.delete_category_button.clicked.connect(self.del_category) if islinux: self.available_items_box.itemDoubleClicked.connect(self.apply_tags) else: - self.connect(self.available_items_box, SIGNAL('itemActivated(QListWidgetItem*)'), self.apply_tags) - self.connect(self.applied_items_box, SIGNAL('itemActivated(QListWidgetItem*)'), self.unapply_tags) + self.connect(self.available_items_box, + SIGNAL('itemActivated(QListWidgetItem*)'), self.apply_tags) + self.connect(self.applied_items_box, + SIGNAL('itemActivated(QListWidgetItem*)'), self.unapply_tags) self.populate_category_list() if on_category is not None: @@ -129,6 +143,7 @@ class TagCategories(QDialog, Ui_TagCategories): n = item.name if item.exists else item.name + _(' (not on any book)') w = QListWidgetItem(item.icon, n) w.setData(Qt.UserRole, item.index) + w.setToolTip(_('Category lookup name: ') + item.label) return w def display_filtered_categories(self, idx): diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py index 8909bc7b3d..cee34f150e 100644 --- a/src/calibre/gui2/preferences/create_custom_column.py +++ b/src/calibre/gui2/preferences/create_custom_column.py @@ -118,6 +118,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): else: sb = 0 self.composite_sort_by.setCurrentIndex(sb) + self.composite_make_category.setChecked( + c['display'].get('make_category', False)) elif ct == 'enumeration': self.enum_box.setText(','.join(c['display'].get('enum_values', []))) self.datatype_changed() @@ -159,7 +161,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): col_type = None for x in ('box', 'default_label', 'label'): getattr(self, 'date_format_'+x).setVisible(col_type == 'datetime') - for x in ('box', 'default_label', 'label', 'sort_by', 'sort_by_label'): + for x in ('box', 'default_label', 'label', 'sort_by', 'sort_by_label', + 'make_category'): getattr(self, 'composite_'+x).setVisible(col_type == 'composite') for x in ('box', 'default_label', 'label'): getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration') @@ -222,7 +225,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): ' composite columns')) display_dict = {'composite_template':unicode(self.composite_box.text()).strip(), 'composite_sort': ['text', 'number', 'date', 'bool'] - [self.composite_sort_by.currentIndex()] + [self.composite_sort_by.currentIndex()], + 'make_category': self.composite_make_category.isChecked(), } elif col_type == 'enumeration': if not unicode(self.enum_box.text()).strip(): diff --git a/src/calibre/gui2/preferences/create_custom_column.ui b/src/calibre/gui2/preferences/create_custom_column.ui index aaa69f5e4b..3290d3c846 100644 --- a/src/calibre/gui2/preferences/create_custom_column.ui +++ b/src/calibre/gui2/preferences/create_custom_column.ui @@ -220,18 +220,18 @@ Everything else will show nothing. - - - - &Sort/search column by - - - composite_sort_by - - - - + + + + + &Sort/search column by + + + composite_sort_by + + + @@ -239,6 +239,16 @@ Everything else will show nothing. + + + + Show in tags browser + + + If checked, this column will appear in the tags browser as a category + + + diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index e867f2cac6..dd8c81d819 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -510,9 +510,11 @@ class TagsView(QTreeView): # {{{ if hasattr(md, 'column_name'): fm_src = self.db.metadata_for_field(md.column_name) if md.column_name in ['authors', 'publisher', 'series'] or \ - (fm_src['is_custom'] and - fm_src['datatype'] in ['series', 'text'] and - not fm_src['is_multiple']): + (fm_src['is_custom'] and ( + (fm_src['datatype'] in ['series', 'text', 'enumeration'] and + not fm_src['is_multiple']) or + (fm_src['datatype'] == 'composite' and + fm_src['display'].get('make_category', False)))): self.setDropIndicatorShown(True) def clear(self): @@ -976,8 +978,11 @@ class TagsModel(QAbstractItemModel): # {{{ fm = self.db.metadata_for_field(node.tag.category) if node.tag.category in \ ('tags', 'series', 'authors', 'rating', 'publisher') or \ - (fm['is_custom'] and \ - fm['datatype'] in ['text', 'rating', 'series']): + (fm['is_custom'] and ( + fm['datatype'] in ['text', 'rating', 'series', + 'enumeration'] or + (fm['datatype'] == 'composite' and + fm['display'].get('make_category', False)))): mime = 'application/calibre+from_library' ids = list(map(int, str(md.data(mime)).split())) self.handle_drop(node, ids) @@ -987,9 +992,11 @@ class TagsModel(QAbstractItemModel): # {{{ if fm_dest['kind'] == 'user': fm_src = self.db.metadata_for_field(md.column_name) if md.column_name in ['authors', 'publisher', 'series'] or \ - (fm_src['is_custom'] and - fm_src['datatype'] in ['series', 'text'] and - not fm_src['is_multiple']): + (fm_src['is_custom'] and ( + (fm_src['datatype'] in ['series', 'text', 'enumeration'] and + not fm_src['is_multiple']))or + (fm_src['datatype'] == 'composite' and + fm_src['display'].get('make_category', False))): mime = 'application/calibre+from_library' ids = list(map(int, str(md.data(mime)).split())) self.handle_user_category_drop(node, ids, md.column_name) @@ -1003,7 +1010,6 @@ class TagsModel(QAbstractItemModel): # {{{ return fm_src = self.db.metadata_for_field(column) for id in ids: - vmap = {} label = fm_src['label'] if not fm_src['is_custom']: if label == 'authors': @@ -1019,19 +1025,21 @@ class TagsModel(QAbstractItemModel): # {{{ value = self.db.series(id, index_is_id=True) else: items = self.db.get_custom_items_with_ids(label=label) - value = self.db.get_custom(id, label=label, index_is_id=True) + if fm_src['datatype'] != 'composite': + value = self.db.get_custom(id, label=label, index_is_id=True) + else: + value = self.db.get_property(id, loc=fm_src['rec_index'], + index_is_id=True) if value is None: return if not isinstance(value, list): value = [value] - for v in items: - vmap[v[1]] = v[0] for val in value: for (v, c, id) in category: if v == val and c == column: break else: - category.append([val, column, vmap[val]]) + category.append([val, column, 0]) categories[on_node.category_key[1:]] = category self.db.prefs.set('user_categories', categories) self.tags_view.recount() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index ec766c72f3..50a0ba98dd 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1210,6 +1210,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return ans field = self.field_metadata[category] + if field['datatype'] == 'composite': + dex = field['rec_index'] + for book in self.data.iterall(): + if book[dex] == id_: + ans.add(book[0]) + return ans + ans = self.conn.get( 'SELECT book FROM books_{tn}_link WHERE {col}=?'.format( tn=field['table'], col=field['link_column']), (id_,)) @@ -1281,7 +1288,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # First, build the maps. We need a category->items map and an # item -> (item_id, sort_val) map to use in the books loop - for category in tb_cats.keys(): + for category in tb_cats.iterkeys(): cat = tb_cats[category] if not cat['is_category'] or cat['kind'] in ['user', 'search'] \ or category in ['news', 'formats'] or cat.get('is_csp', @@ -1324,8 +1331,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): tcategories[category] = {} # create a list of category/field_index for the books scan to use. # This saves iterating through field_metadata for each book - md.append((category, cat['rec_index'], cat['is_multiple'])) + md.append((category, cat['rec_index'], cat['is_multiple'], False)) + for category in tb_cats.iterkeys(): + cat = tb_cats[category] + if cat['datatype'] == 'composite' and \ + cat['display'].get('make_category', False): + tcategories[category] = {} + md.append((category, cat['rec_index'], cat['is_multiple'], + cat['datatype'] == 'composite')) #print 'end phase "collection":', time.clock() - last, 'seconds' #last = time.clock() @@ -1339,11 +1353,22 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): continue rating = book[rating_dex] # We kept track of all possible category field_map positions above - for (cat, dex, mult) in md: - if book[dex] is None: + for (cat, dex, mult, is_comp) in md: + if not book[dex]: continue if not mult: val = book[dex] + if is_comp: + item = tcategories[cat].get(val, None) + if not item: + item = tag_class(val, val) + tcategories[cat][val] = item + item.c += 1 + item.id = val + if rating > 0: + item.rt += rating + item.rc += 1 + continue try: (item_id, sort_val) = tids[cat][val] # let exceptions fly item = tcategories[cat].get(val, None) @@ -1405,7 +1430,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # and building the Tag instances. categories = {} tag_class = Tag - for category in tb_cats.keys(): + for category in tb_cats.iterkeys(): if category not in tcategories: continue cat = tb_cats[category] diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index fd015f5848..c3843840b8 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -623,6 +623,8 @@ class BrowseServer(object): except: raise cherrypy.HTTPError(404, 'Search: %r not understood'%which) else: + if fm[category]['datatype'] == 'composite': + cid = cid.decode('utf-8') all_ids = self.search_cache('') if category == 'newest': ids = all_ids diff --git a/src/calibre/manual/images/sg_genre.jpg b/src/calibre/manual/images/sg_genre.jpg index b9a4537772..5d9658b14d 100644 Binary files a/src/calibre/manual/images/sg_genre.jpg and b/src/calibre/manual/images/sg_genre.jpg differ diff --git a/src/calibre/manual/images/sg_tb.jpg b/src/calibre/manual/images/sg_tb.jpg index eeb5e73c97..63610845db 100644 Binary files a/src/calibre/manual/images/sg_tb.jpg and b/src/calibre/manual/images/sg_tb.jpg differ diff --git a/src/calibre/manual/sub_groups.rst b/src/calibre/manual/sub_groups.rst index edfc81d3d4..c27b3581f8 100644 --- a/src/calibre/manual/sub_groups.rst +++ b/src/calibre/manual/sub_groups.rst @@ -70,7 +70,7 @@ Then after restarting |app|, you must tell |app| that the column is to be treate At the point there are no genres in the column. We are left with the last step: how to apply a genre to a book. A genre does not exist in |app| until it appears on at least one book. To learn how to apply a genre for the first time, we must go into some detail about what a genre looks like in the metadata for a book. -A hierarchy of 'things' is built by creating an item consisting of phrases separated by periods. Continuing the genre example, these items would "History.Military", "Mysteries.Vampire", "Science Fiction.Space Opera", etc. Thus to create a new genre, you pick a book that should have that genre, edit its metadata, and enter the new genre into the column you created. Continuing our example, if you want to assign a new genre "Comics" with a sub-genre "Superheros" to a book, you would 'edit metadata' for that (comic) book, choose the Custom metadata tab, and then enter "Comics.Superheros" as shown in the following (ignore the other custom columns): +A hierarchy of 'things' is built by creating an item consisting of phrases separated by periods. Continuing the genre example, these items would "History.Military", "Mysteries.Vampire", "Science Fiction.Space Opera", etc. Thus to create a new genre, you pick a book that should have that genre, edit its metadata, and enter the new genre into the column you created. Continuing our example, if you want to assign a new genre "Comics" with a sub-genre "Superheroes" to a book, you would 'edit metadata' for that (comic) book, choose the Custom metadata tab, and then enter "Comics.Superheroes" as shown in the following (ignore the other custom columns): .. image:: images/sg_genre.jpg :align: center