mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Add a series type custom column
This commit is contained in:
commit
186532e795
@ -5,7 +5,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import sys
|
import re, sys
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
|
from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
|
||||||
@ -162,7 +162,6 @@ class DateTime(Base):
|
|||||||
val = qt_to_dt(val)
|
val = qt_to_dt(val)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
class Comments(Base):
|
class Comments(Base):
|
||||||
|
|
||||||
def setup_ui(self, parent):
|
def setup_ui(self, parent):
|
||||||
@ -199,11 +198,7 @@ class Text(Base):
|
|||||||
w = EnComboBox(parent)
|
w = EnComboBox(parent)
|
||||||
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
||||||
w.setMinimumContentsLength(25)
|
w.setMinimumContentsLength(25)
|
||||||
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
|
||||||
|
|
||||||
|
|
||||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
|
||||||
w]
|
|
||||||
|
|
||||||
def initialize(self, book_id):
|
def initialize(self, book_id):
|
||||||
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||||
@ -222,7 +217,6 @@ class Text(Base):
|
|||||||
if idx is not None:
|
if idx is not None:
|
||||||
self.widgets[1].setCurrentIndex(idx)
|
self.widgets[1].setCurrentIndex(idx)
|
||||||
|
|
||||||
|
|
||||||
def setter(self, val):
|
def setter(self, val):
|
||||||
if self.col_metadata['is_multiple']:
|
if self.col_metadata['is_multiple']:
|
||||||
if not val:
|
if not val:
|
||||||
@ -241,6 +235,58 @@ class Text(Base):
|
|||||||
val = None
|
val = None
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
class Series(Base):
|
||||||
|
|
||||||
|
def setup_ui(self, parent):
|
||||||
|
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
||||||
|
values.sort(cmp = lambda x,y: cmp(x.lower(), y.lower()))
|
||||||
|
w = EnComboBox(parent)
|
||||||
|
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
||||||
|
w.setMinimumContentsLength(25)
|
||||||
|
self.name_widget = w
|
||||||
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
|
||||||
|
|
||||||
|
self.widgets.append(QLabel('&'+self.col_metadata['name']+_(' index:'), parent))
|
||||||
|
w = QDoubleSpinBox(parent)
|
||||||
|
w.setRange(-100., float(sys.maxint))
|
||||||
|
w.setDecimals(2)
|
||||||
|
w.setSpecialValueText(_('Undefined'))
|
||||||
|
w.setSingleStep(1)
|
||||||
|
self.idx_widget=w
|
||||||
|
self.widgets.append(w)
|
||||||
|
|
||||||
|
def initialize(self, book_id):
|
||||||
|
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||||
|
s_index = self.db.get_custom_extra(book_id, num=self.col_id, index_is_id=True)
|
||||||
|
if s_index is None:
|
||||||
|
s_index = 0.0
|
||||||
|
self.idx_widget.setValue(s_index)
|
||||||
|
self.initial_index = s_index
|
||||||
|
self.initial_val = val
|
||||||
|
val = self.normalize_db_val(val)
|
||||||
|
idx = None
|
||||||
|
for i, c in enumerate(self.all_values):
|
||||||
|
if c == val:
|
||||||
|
idx = i
|
||||||
|
self.name_widget.addItem(c)
|
||||||
|
self.name_widget.setEditText('')
|
||||||
|
if idx is not None:
|
||||||
|
self.widgets[1].setCurrentIndex(idx)
|
||||||
|
|
||||||
|
def commit(self, book_id, notify=False):
|
||||||
|
val = unicode(self.name_widget.currentText()).strip()
|
||||||
|
val = self.normalize_ui_val(val)
|
||||||
|
s_index = self.idx_widget.value()
|
||||||
|
if val != self.initial_val or s_index != self.initial_index:
|
||||||
|
if s_index == 0.0:
|
||||||
|
if tweaks['series_index_auto_increment'] == 'next':
|
||||||
|
s_index = self.db.get_next_cc_series_num_for(val,
|
||||||
|
num=self.col_id)
|
||||||
|
else:
|
||||||
|
s_index = None
|
||||||
|
self.db.set_custom(book_id, val, extra=s_index,
|
||||||
|
num=self.col_id, notify=notify)
|
||||||
|
|
||||||
widgets = {
|
widgets = {
|
||||||
'bool' : Bool,
|
'bool' : Bool,
|
||||||
'rating' : Rating,
|
'rating' : Rating,
|
||||||
@ -249,6 +295,7 @@ widgets = {
|
|||||||
'datetime': DateTime,
|
'datetime': DateTime,
|
||||||
'text' : Text,
|
'text' : Text,
|
||||||
'comments': Comments,
|
'comments': Comments,
|
||||||
|
'series': Series,
|
||||||
}
|
}
|
||||||
|
|
||||||
def field_sort(y, z, x=None):
|
def field_sort(y, z, x=None):
|
||||||
@ -257,35 +304,63 @@ def field_sort(y, z, x=None):
|
|||||||
n2 = 'zzzzz' if m2['datatype'] == 'comments' else m2['name']
|
n2 = 'zzzzz' if m2['datatype'] == 'comments' else m2['name']
|
||||||
return cmp(n1.lower(), n2.lower())
|
return cmp(n1.lower(), n2.lower())
|
||||||
|
|
||||||
def populate_single_metadata_page(left, right, db, book_id, parent=None):
|
def populate_metadata_page(layout, db, book_id, bulk=False, two_column=False, parent=None):
|
||||||
|
def widget_factory(type, col):
|
||||||
|
if bulk:
|
||||||
|
w = bulk_widgets[type](db, col, parent)
|
||||||
|
else:
|
||||||
|
w = widgets[type](db, col, parent)
|
||||||
|
w.initialize(book_id)
|
||||||
|
return w
|
||||||
x = db.custom_column_num_map
|
x = db.custom_column_num_map
|
||||||
cols = list(x)
|
cols = list(x)
|
||||||
cols.sort(cmp=partial(field_sort, x=x))
|
cols.sort(cmp=partial(field_sort, x=x))
|
||||||
|
count_non_comment = len([c for c in cols if x[c]['datatype'] != 'comments'])
|
||||||
|
|
||||||
|
layout.setColumnStretch(1, 10)
|
||||||
|
if two_column:
|
||||||
|
turnover_point = (count_non_comment+1)/2
|
||||||
|
layout.setColumnStretch(3, 10)
|
||||||
|
else:
|
||||||
|
# Avoid problems with multi-line widgets
|
||||||
|
turnover_point = count_non_comment + 1000
|
||||||
ans = []
|
ans = []
|
||||||
for i, col in enumerate(cols):
|
column = row = 0
|
||||||
w = widgets[x[col]['datatype']](db, col, parent)
|
for col in cols:
|
||||||
|
dt = x[col]['datatype']
|
||||||
|
if dt == 'comments':
|
||||||
|
continue
|
||||||
|
w = widget_factory(dt, col)
|
||||||
ans.append(w)
|
ans.append(w)
|
||||||
w.initialize(book_id)
|
for c in range(0, len(w.widgets), 2):
|
||||||
layout = left if i%2 == 0 else right
|
w.widgets[c].setBuddy(w.widgets[c+1])
|
||||||
row = layout.rowCount()
|
layout.addWidget(w.widgets[c], row, column)
|
||||||
if len(w.widgets) == 1:
|
layout.addWidget(w.widgets[c+1], row, column+1)
|
||||||
layout.addWidget(w.widgets[0], row, 0, 1, -1)
|
row += 1
|
||||||
else:
|
if row >= turnover_point:
|
||||||
w.widgets[0].setBuddy(w.widgets[1])
|
column += 2
|
||||||
for c, widget in enumerate(w.widgets):
|
turnover_point = count_non_comment + 1000
|
||||||
layout.addWidget(widget, row, c)
|
row = 0
|
||||||
|
if not bulk: # Add the comments fields
|
||||||
|
column = 0
|
||||||
|
for col in cols:
|
||||||
|
dt = x[col]['datatype']
|
||||||
|
if dt != 'comments':
|
||||||
|
continue
|
||||||
|
w = widget_factory(dt, col)
|
||||||
|
ans.append(w)
|
||||||
|
layout.addWidget(w.widgets[0], row, column, 1, 2)
|
||||||
|
if two_column and column == 0:
|
||||||
|
column = 2
|
||||||
|
continue
|
||||||
|
column = 0
|
||||||
|
row += 1
|
||||||
items = []
|
items = []
|
||||||
if len(ans) > 0:
|
if len(ans) > 0:
|
||||||
items.append(QSpacerItem(10, 10, QSizePolicy.Minimum,
|
items.append(QSpacerItem(10, 10, QSizePolicy.Minimum,
|
||||||
QSizePolicy.Expanding))
|
QSizePolicy.Expanding))
|
||||||
left.addItem(items[-1], left.rowCount(), 0, 1, 1)
|
layout.addItem(items[-1], layout.rowCount(), 0, 1, 1)
|
||||||
left.setRowStretch(left.rowCount()-1, 100)
|
layout.setRowStretch(layout.rowCount()-1, 100)
|
||||||
if len(ans) > 1:
|
|
||||||
items.append(QSpacerItem(10, 100, QSizePolicy.Minimum,
|
|
||||||
QSizePolicy.Expanding))
|
|
||||||
right.addItem(items[-1], left.rowCount(), 0, 1, 1)
|
|
||||||
right.setRowStretch(right.rowCount()-1, 100)
|
|
||||||
|
|
||||||
return ans, items
|
return ans, items
|
||||||
|
|
||||||
class BulkBase(Base):
|
class BulkBase(Base):
|
||||||
@ -342,6 +417,47 @@ class BulkRating(BulkBase, Rating):
|
|||||||
class BulkDateTime(BulkBase, DateTime):
|
class BulkDateTime(BulkBase, DateTime):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class BulkSeries(BulkBase):
|
||||||
|
def setup_ui(self, parent):
|
||||||
|
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
||||||
|
values.sort(cmp = lambda x,y: cmp(x.lower(), y.lower()))
|
||||||
|
w = EnComboBox(parent)
|
||||||
|
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
||||||
|
w.setMinimumContentsLength(25)
|
||||||
|
self.name_widget = w
|
||||||
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
|
||||||
|
|
||||||
|
self.widgets.append(QLabel(_('Automatically number books in this series'), parent))
|
||||||
|
self.idx_widget=QCheckBox(parent)
|
||||||
|
self.widgets.append(self.idx_widget)
|
||||||
|
|
||||||
|
def initialize(self, book_id):
|
||||||
|
self.idx_widget.setChecked(False)
|
||||||
|
for c in self.all_values:
|
||||||
|
self.name_widget.addItem(c)
|
||||||
|
self.name_widget.setEditText('')
|
||||||
|
|
||||||
|
def commit(self, book_ids, notify=False):
|
||||||
|
val = unicode(self.name_widget.currentText()).strip()
|
||||||
|
val = self.normalize_ui_val(val)
|
||||||
|
update_indices = self.idx_widget.checkState()
|
||||||
|
if val != '':
|
||||||
|
for book_id in book_ids:
|
||||||
|
if update_indices:
|
||||||
|
if tweaks['series_index_auto_increment'] == 'next':
|
||||||
|
s_index = self.db.get_next_cc_series_num_for\
|
||||||
|
(val, num=self.col_id)
|
||||||
|
else:
|
||||||
|
s_index = 1.0
|
||||||
|
else:
|
||||||
|
s_index = self.db.get_custom_extra(book_id, num=self.col_id,
|
||||||
|
index_is_id=True)
|
||||||
|
self.db.set_custom(book_id, val, extra=s_index,
|
||||||
|
num=self.col_id, notify=notify)
|
||||||
|
|
||||||
|
def process_each_book(self):
|
||||||
|
return True
|
||||||
|
|
||||||
class RemoveTags(QWidget):
|
class RemoveTags(QWidget):
|
||||||
|
|
||||||
def __init__(self, parent, values):
|
def __init__(self, parent, values):
|
||||||
@ -431,35 +547,5 @@ bulk_widgets = {
|
|||||||
'float': BulkFloat,
|
'float': BulkFloat,
|
||||||
'datetime': BulkDateTime,
|
'datetime': BulkDateTime,
|
||||||
'text' : BulkText,
|
'text' : BulkText,
|
||||||
|
'series': BulkSeries,
|
||||||
}
|
}
|
||||||
|
|
||||||
def populate_bulk_metadata_page(layout, db, book_ids, parent=None):
|
|
||||||
x = db.custom_column_num_map
|
|
||||||
cols = list(x)
|
|
||||||
cols.sort(cmp=partial(field_sort, x=x))
|
|
||||||
ans = []
|
|
||||||
for i, col in enumerate(cols):
|
|
||||||
dt = x[col]['datatype']
|
|
||||||
if dt == 'comments':
|
|
||||||
continue
|
|
||||||
w = bulk_widgets[dt](db, col, parent)
|
|
||||||
ans.append(w)
|
|
||||||
w.initialize(book_ids)
|
|
||||||
row = layout.rowCount()
|
|
||||||
if len(w.widgets) == 1:
|
|
||||||
layout.addWidget(w.widgets[0], row, 0, 1, -1)
|
|
||||||
else:
|
|
||||||
for c in range(0, len(w.widgets), 2):
|
|
||||||
w.widgets[c].setBuddy(w.widgets[c+1])
|
|
||||||
layout.addWidget(w.widgets[c], row, 0)
|
|
||||||
layout.addWidget(w.widgets[c+1], row, 1)
|
|
||||||
row += 1
|
|
||||||
items = []
|
|
||||||
if len(ans) > 0:
|
|
||||||
items.append(QSpacerItem(10, 10, QSizePolicy.Minimum,
|
|
||||||
QSizePolicy.Expanding))
|
|
||||||
layout.addItem(items[-1], layout.rowCount(), 0, 1, 1)
|
|
||||||
layout.setRowStretch(layout.rowCount()-1, 100)
|
|
||||||
|
|
||||||
return ans, items
|
|
||||||
|
|
||||||
|
@ -24,16 +24,19 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
|||||||
2:{'datatype':'comments',
|
2:{'datatype':'comments',
|
||||||
'text':_('Long text, like comments, not shown in the tag browser'),
|
'text':_('Long text, like comments, not shown in the tag browser'),
|
||||||
'is_multiple':False},
|
'is_multiple':False},
|
||||||
3:{'datatype':'datetime',
|
3:{'datatype':'series',
|
||||||
|
'text':_('Text column for keeping series-like information'),
|
||||||
|
'is_multiple':False},
|
||||||
|
4:{'datatype':'datetime',
|
||||||
'text':_('Date'), 'is_multiple':False},
|
'text':_('Date'), 'is_multiple':False},
|
||||||
4:{'datatype':'float',
|
5:{'datatype':'float',
|
||||||
'text':_('Floating point numbers'), 'is_multiple':False},
|
'text':_('Floating point numbers'), 'is_multiple':False},
|
||||||
5:{'datatype':'int',
|
6:{'datatype':'int',
|
||||||
'text':_('Integers'), 'is_multiple':False},
|
'text':_('Integers'), 'is_multiple':False},
|
||||||
6:{'datatype':'rating',
|
7:{'datatype':'rating',
|
||||||
'text':_('Ratings, shown with stars'),
|
'text':_('Ratings, shown with stars'),
|
||||||
'is_multiple':False},
|
'is_multiple':False},
|
||||||
7:{'datatype':'bool',
|
8:{'datatype':'bool',
|
||||||
'text':_('Yes/No'), 'is_multiple':False},
|
'text':_('Yes/No'), 'is_multiple':False},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
|
|||||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||||
from calibre.ebooks.metadata import string_to_authors, \
|
from calibre.ebooks.metadata import string_to_authors, \
|
||||||
authors_to_string
|
authors_to_string
|
||||||
from calibre.gui2.custom_column_widgets import populate_bulk_metadata_page
|
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||||
|
|
||||||
class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||||
|
|
||||||
@ -44,15 +44,14 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
|||||||
self.central_widget.tabBar().setVisible(False)
|
self.central_widget.tabBar().setVisible(False)
|
||||||
else:
|
else:
|
||||||
self.create_custom_column_editors()
|
self.create_custom_column_editors()
|
||||||
|
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
def create_custom_column_editors(self):
|
def create_custom_column_editors(self):
|
||||||
w = self.central_widget.widget(1)
|
w = self.central_widget.widget(1)
|
||||||
layout = QGridLayout()
|
layout = QGridLayout()
|
||||||
|
self.custom_column_widgets, self.__cc_spacers = \
|
||||||
self.custom_column_widgets, self.__cc_spacers = populate_bulk_metadata_page(
|
populate_metadata_page(layout, self.db, self.ids, parent=w,
|
||||||
layout, self.db, self.ids, w)
|
two_column=False, bulk=True)
|
||||||
w.setLayout(layout)
|
w.setLayout(layout)
|
||||||
self.__custom_col_layouts = [layout]
|
self.__custom_col_layouts = [layout]
|
||||||
ans = self.custom_column_widgets
|
ans = self.custom_column_widgets
|
||||||
|
@ -32,7 +32,7 @@ from calibre.utils.config import prefs, tweaks
|
|||||||
from calibre.utils.date import qt_to_dt
|
from calibre.utils.date import qt_to_dt
|
||||||
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
|
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
|
||||||
from calibre.gui2.dialogs.config.social import SocialMetadata
|
from calibre.gui2.dialogs.config.social import SocialMetadata
|
||||||
from calibre.gui2.custom_column_widgets import populate_single_metadata_page
|
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||||
|
|
||||||
class CoverFetcher(QThread):
|
class CoverFetcher(QThread):
|
||||||
|
|
||||||
@ -420,23 +420,19 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
|
|
||||||
def create_custom_column_editors(self):
|
def create_custom_column_editors(self):
|
||||||
w = self.central_widget.widget(1)
|
w = self.central_widget.widget(1)
|
||||||
top_layout = QHBoxLayout()
|
layout = w.layout()
|
||||||
top_layout.setSpacing(20)
|
self.custom_column_widgets, self.__cc_spacers = \
|
||||||
left_layout = QGridLayout()
|
populate_metadata_page(layout, self.db, self.id,
|
||||||
right_layout = QGridLayout()
|
parent=w, bulk=False, two_column=True)
|
||||||
top_layout.addLayout(left_layout)
|
self.__custom_col_layouts = [layout]
|
||||||
|
|
||||||
self.custom_column_widgets, self.__cc_spacers = populate_single_metadata_page(
|
|
||||||
left_layout, right_layout, self.db, self.id, w)
|
|
||||||
top_layout.addLayout(right_layout)
|
|
||||||
sip.delete(w.layout())
|
|
||||||
w.setLayout(top_layout)
|
|
||||||
self.__custom_col_layouts = [top_layout, left_layout, right_layout]
|
|
||||||
ans = self.custom_column_widgets
|
ans = self.custom_column_widgets
|
||||||
for i in range(len(ans)-1):
|
for i in range(len(ans)-1):
|
||||||
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[-1])
|
if len(ans[i+1].widgets) == 2:
|
||||||
|
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1])
|
||||||
|
else:
|
||||||
|
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[0])
|
||||||
|
for c in range(2, len(ans[i].widgets), 2):
|
||||||
|
w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])
|
||||||
|
|
||||||
def validate_isbn(self, isbn):
|
def validate_isbn(self, isbn):
|
||||||
isbn = unicode(isbn).strip()
|
isbn = unicode(isbn).strip()
|
||||||
|
@ -520,7 +520,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
return QVariant(', '.join(sorted(tags.split(','))))
|
return QVariant(', '.join(sorted(tags.split(','))))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def series(r, idx=-1, siix=-1):
|
def series_type(r, idx=-1, siix=-1):
|
||||||
series = self.db.data[r][idx]
|
series = self.db.data[r][idx]
|
||||||
if series:
|
if series:
|
||||||
idx = fmt_sidx(self.db.data[r][siix])
|
idx = fmt_sidx(self.db.data[r][siix])
|
||||||
@ -591,7 +591,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
idx=self.db.field_metadata['publisher']['rec_index'], mult=False),
|
idx=self.db.field_metadata['publisher']['rec_index'], mult=False),
|
||||||
'tags' : functools.partial(tags,
|
'tags' : functools.partial(tags,
|
||||||
idx=self.db.field_metadata['tags']['rec_index']),
|
idx=self.db.field_metadata['tags']['rec_index']),
|
||||||
'series' : functools.partial(series,
|
'series' : functools.partial(series_type,
|
||||||
idx=self.db.field_metadata['series']['rec_index'],
|
idx=self.db.field_metadata['series']['rec_index'],
|
||||||
siix=self.db.field_metadata['series_index']['rec_index']),
|
siix=self.db.field_metadata['series_index']['rec_index']),
|
||||||
'ondevice' : functools.partial(text_type,
|
'ondevice' : functools.partial(text_type,
|
||||||
@ -620,6 +620,9 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] == 'yes')
|
bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] == 'yes')
|
||||||
elif datatype == 'rating':
|
elif datatype == 'rating':
|
||||||
self.dc[col] = functools.partial(rating_type, idx=idx)
|
self.dc[col] = functools.partial(rating_type, idx=idx)
|
||||||
|
elif datatype == 'series':
|
||||||
|
self.dc[col] = functools.partial(series_type, idx=idx,
|
||||||
|
siix=self.db.field_metadata.cc_series_index_column_for(col))
|
||||||
else:
|
else:
|
||||||
print 'What type is this?', col, datatype
|
print 'What type is this?', col, datatype
|
||||||
# build a index column to data converter map, to remove the string lookup in the data loop
|
# build a index column to data converter map, to remove the string lookup in the data loop
|
||||||
@ -681,6 +684,8 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
|
|
||||||
def set_custom_column_data(self, row, colhead, value):
|
def set_custom_column_data(self, row, colhead, value):
|
||||||
typ = self.custom_columns[colhead]['datatype']
|
typ = self.custom_columns[colhead]['datatype']
|
||||||
|
label=self.db.field_metadata.key_to_label(colhead)
|
||||||
|
s_index = None
|
||||||
if typ in ('text', 'comments'):
|
if typ in ('text', 'comments'):
|
||||||
val = unicode(value.toString()).strip()
|
val = unicode(value.toString()).strip()
|
||||||
val = val if val else None
|
val = val if val else None
|
||||||
@ -702,9 +707,20 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
if not val.isValid():
|
if not val.isValid():
|
||||||
return False
|
return False
|
||||||
val = qt_to_dt(val, as_utc=False)
|
val = qt_to_dt(val, as_utc=False)
|
||||||
self.db.set_custom(self.db.id(row), val,
|
elif typ == 'series':
|
||||||
label=self.db.field_metadata.key_to_label(colhead),
|
val = unicode(value.toString()).strip()
|
||||||
num=None, append=False, notify=True)
|
pat = re.compile(r'\[([.0-9]+)\]')
|
||||||
|
match = pat.search(val)
|
||||||
|
if match is not None:
|
||||||
|
val = pat.sub('', val).strip()
|
||||||
|
s_index = float(match.group(1))
|
||||||
|
elif val:
|
||||||
|
if tweaks['series_index_auto_increment'] == 'next':
|
||||||
|
s_index = self.db.get_next_cc_series_num_for(val, label=label)
|
||||||
|
else:
|
||||||
|
s_index = 1.0
|
||||||
|
self.db.set_custom(self.db.id(row), val, extra=s_index,
|
||||||
|
label=label, num=None, append=False, notify=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def setData(self, index, value, role):
|
def setData(self, index, value, role):
|
||||||
|
@ -347,7 +347,7 @@ class BooksView(QTableView): # {{{
|
|||||||
self.setItemDelegateForColumn(cm.index(colhead), delegate)
|
self.setItemDelegateForColumn(cm.index(colhead), delegate)
|
||||||
elif cc['datatype'] == 'comments':
|
elif cc['datatype'] == 'comments':
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate)
|
self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate)
|
||||||
elif cc['datatype'] == 'text':
|
elif cc['datatype'] in ('text', 'series'):
|
||||||
if cc['is_multiple']:
|
if cc['is_multiple']:
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), self.tags_delegate)
|
self.setItemDelegateForColumn(cm.index(colhead), self.tags_delegate)
|
||||||
else:
|
else:
|
||||||
|
@ -401,7 +401,8 @@ class ResultCache(SearchQueryParser):
|
|||||||
for x in self.field_metadata:
|
for x in self.field_metadata:
|
||||||
if len(self.field_metadata[x]['search_terms']):
|
if len(self.field_metadata[x]['search_terms']):
|
||||||
db_col[x] = self.field_metadata[x]['rec_index']
|
db_col[x] = self.field_metadata[x]['rec_index']
|
||||||
if self.field_metadata[x]['datatype'] not in ['text', 'comments']:
|
if self.field_metadata[x]['datatype'] not in \
|
||||||
|
['text', 'comments', 'series']:
|
||||||
exclude_fields.append(db_col[x])
|
exclude_fields.append(db_col[x])
|
||||||
col_datatype[db_col[x]] = self.field_metadata[x]['datatype']
|
col_datatype[db_col[x]] = self.field_metadata[x]['datatype']
|
||||||
is_multiple_cols[db_col[x]] = self.field_metadata[x]['is_multiple']
|
is_multiple_cols[db_col[x]] = self.field_metadata[x]['is_multiple']
|
||||||
@ -580,16 +581,18 @@ class ResultCache(SearchQueryParser):
|
|||||||
self.sort(field, ascending)
|
self.sort(field, ascending)
|
||||||
self._map_filtered = list(self._map)
|
self._map_filtered = list(self._map)
|
||||||
|
|
||||||
def seriescmp(self, x, y):
|
def seriescmp(self, sidx, siidx, x, y, library_order=None):
|
||||||
sidx = self.FIELD_MAP['series']
|
|
||||||
try:
|
try:
|
||||||
ans = cmp(title_sort(self._data[x][sidx].lower()),
|
if library_order:
|
||||||
title_sort(self._data[y][sidx].lower()))
|
ans = cmp(title_sort(self._data[x][sidx].lower()),
|
||||||
|
title_sort(self._data[y][sidx].lower()))
|
||||||
|
else:
|
||||||
|
ans = cmp(self._data[x][sidx].lower(),
|
||||||
|
self._data[y][sidx].lower())
|
||||||
except AttributeError: # Some entries may be None
|
except AttributeError: # Some entries may be None
|
||||||
ans = cmp(self._data[x][sidx], self._data[y][sidx])
|
ans = cmp(self._data[x][sidx], self._data[y][sidx])
|
||||||
if ans != 0: return ans
|
if ans != 0: return ans
|
||||||
sidx = self.FIELD_MAP['series_index']
|
return cmp(self._data[x][siidx], self._data[y][siidx])
|
||||||
return cmp(self._data[x][sidx], self._data[y][sidx])
|
|
||||||
|
|
||||||
def cmp(self, loc, x, y, asstr=True, subsort=False):
|
def cmp(self, loc, x, y, asstr=True, subsort=False):
|
||||||
try:
|
try:
|
||||||
@ -618,18 +621,27 @@ class ResultCache(SearchQueryParser):
|
|||||||
elif field == 'authors': field = 'author_sort'
|
elif field == 'authors': field = 'author_sort'
|
||||||
as_string = field not in ('size', 'rating', 'timestamp')
|
as_string = field not in ('size', 'rating', 'timestamp')
|
||||||
if self.field_metadata[field]['is_custom']:
|
if self.field_metadata[field]['is_custom']:
|
||||||
as_string = self.field_metadata[field]['datatype'] in ('comments', 'text')
|
if self.field_metadata[field]['datatype'] == 'series':
|
||||||
field = self.field_metadata[field]['colnum']
|
fcmp = functools.partial(self.seriescmp,
|
||||||
|
self.field_metadata[field]['rec_index'],
|
||||||
|
self.field_metadata.cc_series_index_column_for(field),
|
||||||
|
library_order=tweaks['title_series_sorting'] == 'library_order')
|
||||||
|
else:
|
||||||
|
as_string = self.field_metadata[field]['datatype'] in ('comments', 'text')
|
||||||
|
field = self.field_metadata[field]['colnum']
|
||||||
|
fcmp = functools.partial(self.cmp, self.FIELD_MAP[field],
|
||||||
|
subsort=subsort, asstr=as_string)
|
||||||
|
elif field == 'series':
|
||||||
|
fcmp = functools.partial(self.seriescmp, self.FIELD_MAP['series'],
|
||||||
|
self.FIELD_MAP['series_index'],
|
||||||
|
library_order=tweaks['title_series_sorting'] == 'library_order')
|
||||||
|
else:
|
||||||
|
fcmp = functools.partial(self.cmp, self.FIELD_MAP[field],
|
||||||
|
subsort=subsort, asstr=as_string)
|
||||||
|
|
||||||
if self.first_sort:
|
if self.first_sort:
|
||||||
subsort = True
|
subsort = True
|
||||||
self.first_sort = False
|
self.first_sort = False
|
||||||
fcmp = self.seriescmp \
|
|
||||||
if field == 'series' and \
|
|
||||||
tweaks['title_series_sorting'] == 'library_order' \
|
|
||||||
else \
|
|
||||||
functools.partial(self.cmp, self.FIELD_MAP[field],
|
|
||||||
subsort=subsort, asstr=as_string)
|
|
||||||
self._map.sort(cmp=fcmp, reverse=not ascending)
|
self._map.sort(cmp=fcmp, reverse=not ascending)
|
||||||
self._map_filtered = [id for id in self._map if id in self._map_filtered]
|
self._map_filtered = [id for id in self._map if id in self._map_filtered]
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from math import floor
|
||||||
|
|
||||||
from calibre import prints
|
from calibre import prints
|
||||||
from calibre.constants import preferred_encoding
|
from calibre.constants import preferred_encoding
|
||||||
@ -16,7 +17,7 @@ from calibre.utils.date import parse_date
|
|||||||
class CustomColumns(object):
|
class CustomColumns(object):
|
||||||
|
|
||||||
CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
|
CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
|
||||||
'int', 'float', 'bool'])
|
'int', 'float', 'bool', 'series'])
|
||||||
|
|
||||||
def custom_table_names(self, num):
|
def custom_table_names(self, num):
|
||||||
return 'custom_column_%d'%num, 'books_custom_column_%d_link'%num
|
return 'custom_column_%d'%num, 'books_custom_column_%d_link'%num
|
||||||
@ -137,7 +138,8 @@ class CustomColumns(object):
|
|||||||
'bool': adapt_bool,
|
'bool': adapt_bool,
|
||||||
'comments': lambda x,d: adapt_text(x, {'is_multiple':False}),
|
'comments': lambda x,d: adapt_text(x, {'is_multiple':False}),
|
||||||
'datetime' : adapt_datetime,
|
'datetime' : adapt_datetime,
|
||||||
'text':adapt_text
|
'text':adapt_text,
|
||||||
|
'series':adapt_text
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create Tag Browser categories for custom columns
|
# Create Tag Browser categories for custom columns
|
||||||
@ -171,6 +173,19 @@ class CustomColumns(object):
|
|||||||
ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
|
ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
def get_custom_extra(self, idx, label=None, num=None, index_is_id=False):
|
||||||
|
if label is not None:
|
||||||
|
data = self.custom_column_label_map[label]
|
||||||
|
if num is not None:
|
||||||
|
data = self.custom_column_num_map[num]
|
||||||
|
# add future datatypes with an extra column here
|
||||||
|
if data['datatype'] not in ['series']:
|
||||||
|
return None
|
||||||
|
ign,lt = self.custom_table_names(data['num'])
|
||||||
|
idx = idx if index_is_id else self.id(idx)
|
||||||
|
return self.conn.get('''SELECT extra FROM %s
|
||||||
|
WHERE book=?'''%lt, (idx,), all=False)
|
||||||
|
|
||||||
# convenience methods for tag editing
|
# convenience methods for tag editing
|
||||||
def get_custom_items_with_ids(self, label=None, num=None):
|
def get_custom_items_with_ids(self, label=None, num=None):
|
||||||
if label is not None:
|
if label is not None:
|
||||||
@ -220,6 +235,28 @@ class CustomColumns(object):
|
|||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
# end convenience methods
|
# end convenience methods
|
||||||
|
|
||||||
|
def get_next_cc_series_num_for(self, series, label=None, num=None):
|
||||||
|
if label is not None:
|
||||||
|
data = self.custom_column_label_map[label]
|
||||||
|
if num is not None:
|
||||||
|
data = self.custom_column_num_map[num]
|
||||||
|
if data['datatype'] != 'series':
|
||||||
|
return None
|
||||||
|
table, lt = self.custom_table_names(data['num'])
|
||||||
|
# get the id of the row containing the series string
|
||||||
|
series_id = self.conn.get('SELECT id from %s WHERE value=?'%table,
|
||||||
|
(series,), all=False)
|
||||||
|
if series_id is None:
|
||||||
|
return 1.0
|
||||||
|
# get the label of the associated series number table
|
||||||
|
series_num = self.conn.get('''
|
||||||
|
SELECT MAX({lt}.extra) FROM {lt}
|
||||||
|
WHERE {lt}.book IN (SELECT book FROM {lt} where value=?)
|
||||||
|
'''.format(lt=lt), (series_id,), all=False)
|
||||||
|
if series_num is None:
|
||||||
|
return 1.0
|
||||||
|
return floor(series_num+1)
|
||||||
|
|
||||||
def all_custom(self, label=None, num=None):
|
def all_custom(self, label=None, num=None):
|
||||||
if label is not None:
|
if label is not None:
|
||||||
data = self.custom_column_label_map[label]
|
data = self.custom_column_label_map[label]
|
||||||
@ -271,9 +308,8 @@ class CustomColumns(object):
|
|||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
def set_custom(self, id_, val, extra=None, label=None, num=None,
|
||||||
|
append=False, notify=True):
|
||||||
def set_custom(self, id_, val, label=None, num=None, append=False, notify=True):
|
|
||||||
if label is not None:
|
if label is not None:
|
||||||
data = self.custom_column_label_map[label]
|
data = self.custom_column_label_map[label]
|
||||||
if num is not None:
|
if num is not None:
|
||||||
@ -317,10 +353,17 @@ class CustomColumns(object):
|
|||||||
'INSERT INTO %s(value) VALUES(?)'%table, (x,)).lastrowid
|
'INSERT INTO %s(value) VALUES(?)'%table, (x,)).lastrowid
|
||||||
if not self.conn.get(
|
if not self.conn.get(
|
||||||
'SELECT book FROM %s WHERE book=? AND value=?'%lt,
|
'SELECT book FROM %s WHERE book=? AND value=?'%lt,
|
||||||
(id_, xid), all=False):
|
(id_, xid), all=False):
|
||||||
self.conn.execute(
|
if data['datatype'] == 'series':
|
||||||
'INSERT INTO %s(book, value) VALUES (?,?)'%lt,
|
self.conn.execute(
|
||||||
(id_, xid))
|
'''INSERT INTO %s(book, value, extra)
|
||||||
|
VALUES (?,?,?)'''%lt, (id_, xid, extra))
|
||||||
|
self.data.set(id_, self.FIELD_MAP[data['num']]+1,
|
||||||
|
extra, row_is_id=True)
|
||||||
|
else:
|
||||||
|
self.conn.execute(
|
||||||
|
'''INSERT INTO %s(book, value)
|
||||||
|
VALUES (?,?)'''%lt, (id_, xid))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
nval = self.conn.get(
|
nval = self.conn.get(
|
||||||
'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'],
|
'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'],
|
||||||
@ -370,6 +413,9 @@ class CustomColumns(object):
|
|||||||
{table} ON(link.value={table}.id) WHERE link.book=books.id)
|
{table} ON(link.value={table}.id) WHERE link.book=books.id)
|
||||||
custom_{num}
|
custom_{num}
|
||||||
'''.format(query=query%table, lt=lt, table=table, num=data['num'])
|
'''.format(query=query%table, lt=lt, table=table, num=data['num'])
|
||||||
|
if data['datatype'] == 'series':
|
||||||
|
line += ''',(SELECT extra FROM {lt} WHERE {lt}.book=books.id)
|
||||||
|
custom_index_{num}'''.format(lt=lt, num=data['num'])
|
||||||
else:
|
else:
|
||||||
line = '''
|
line = '''
|
||||||
(SELECT value FROM {table} WHERE book=books.id) custom_{num}
|
(SELECT value FROM {table} WHERE book=books.id) custom_{num}
|
||||||
@ -393,7 +439,7 @@ class CustomColumns(object):
|
|||||||
|
|
||||||
if datatype in ('rating', 'int'):
|
if datatype in ('rating', 'int'):
|
||||||
dt = 'INT'
|
dt = 'INT'
|
||||||
elif datatype in ('text', 'comments'):
|
elif datatype in ('text', 'comments', 'series'):
|
||||||
dt = 'TEXT'
|
dt = 'TEXT'
|
||||||
elif datatype in ('float',):
|
elif datatype in ('float',):
|
||||||
dt = 'REAL'
|
dt = 'REAL'
|
||||||
@ -404,6 +450,10 @@ class CustomColumns(object):
|
|||||||
collate = 'COLLATE NOCASE' if dt == 'TEXT' else ''
|
collate = 'COLLATE NOCASE' if dt == 'TEXT' else ''
|
||||||
table, lt = self.custom_table_names(num)
|
table, lt = self.custom_table_names(num)
|
||||||
if normalized:
|
if normalized:
|
||||||
|
if datatype == 'series':
|
||||||
|
s_index = 'extra REAL,'
|
||||||
|
else:
|
||||||
|
s_index = ''
|
||||||
lines = [
|
lines = [
|
||||||
'''\
|
'''\
|
||||||
CREATE TABLE %s(
|
CREATE TABLE %s(
|
||||||
@ -419,8 +469,9 @@ class CustomColumns(object):
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
book INTEGER NOT NULL,
|
book INTEGER NOT NULL,
|
||||||
value INTEGER NOT NULL,
|
value INTEGER NOT NULL,
|
||||||
|
%s
|
||||||
UNIQUE(book, value)
|
UNIQUE(book, value)
|
||||||
);'''%lt,
|
);'''%(lt, s_index),
|
||||||
|
|
||||||
'CREATE INDEX %s_aidx ON %s (value);'%(lt,lt),
|
'CREATE INDEX %s_aidx ON %s (value);'%(lt,lt),
|
||||||
'CREATE INDEX %s_bidx ON %s (book);'%(lt,lt),
|
'CREATE INDEX %s_bidx ON %s (book);'%(lt,lt),
|
||||||
@ -468,7 +519,7 @@ class CustomColumns(object):
|
|||||||
ratings as r
|
ratings as r
|
||||||
WHERE {lt}.value={table}.id and bl.book={lt}.book and
|
WHERE {lt}.value={table}.id and bl.book={lt}.book and
|
||||||
r.id = bl.rating and r.rating <> 0) avg_rating,
|
r.id = bl.rating and r.rating <> 0) avg_rating,
|
||||||
value AS sort
|
value AS sort
|
||||||
FROM {table};
|
FROM {table};
|
||||||
|
|
||||||
CREATE VIEW tag_browser_filtered_{table} AS SELECT
|
CREATE VIEW tag_browser_filtered_{table} AS SELECT
|
||||||
@ -483,7 +534,7 @@ class CustomColumns(object):
|
|||||||
WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
|
WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
|
||||||
r.id = bl.rating AND r.rating <> 0 AND
|
r.id = bl.rating AND r.rating <> 0 AND
|
||||||
books_list_filter(bl.book)) avg_rating,
|
books_list_filter(bl.book)) avg_rating,
|
||||||
value AS sort
|
value AS sort
|
||||||
FROM {table};
|
FROM {table};
|
||||||
|
|
||||||
'''.format(lt=lt, table=table),
|
'''.format(lt=lt, table=table),
|
||||||
|
@ -237,6 +237,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.custom_column_num_map[col]['label'],
|
self.custom_column_num_map[col]['label'],
|
||||||
base,
|
base,
|
||||||
prefer_custom=True)
|
prefer_custom=True)
|
||||||
|
if self.custom_column_num_map[col]['datatype'] == 'series':
|
||||||
|
# account for the series index column. Field_metadata knows that
|
||||||
|
# the series index is one larger than the series. If you change
|
||||||
|
# it here, be sure to change it there as well.
|
||||||
|
self.FIELD_MAP[str(col)+'_s_index'] = base = base+1
|
||||||
|
|
||||||
self.FIELD_MAP['cover'] = base+1
|
self.FIELD_MAP['cover'] = base+1
|
||||||
self.field_metadata.set_field_record_index('cover', base+1, prefer_custom=False)
|
self.field_metadata.set_field_record_index('cover', base+1, prefer_custom=False)
|
||||||
|
@ -81,7 +81,7 @@ class FieldMetadata(dict):
|
|||||||
'column':'name',
|
'column':'name',
|
||||||
'link_column':'series',
|
'link_column':'series',
|
||||||
'category_sort':'(title_sort(name))',
|
'category_sort':'(title_sort(name))',
|
||||||
'datatype':'text',
|
'datatype':'series',
|
||||||
'is_multiple':None,
|
'is_multiple':None,
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
'name':_('Series'),
|
'name':_('Series'),
|
||||||
@ -398,6 +398,8 @@ class FieldMetadata(dict):
|
|||||||
if val['is_category'] and val['kind'] in ('user', 'search'):
|
if val['is_category'] and val['kind'] in ('user', 'search'):
|
||||||
del self._tb_cats[key]
|
del self._tb_cats[key]
|
||||||
|
|
||||||
|
def cc_series_index_column_for(self, key):
|
||||||
|
return self._tb_cats[key]['rec_index'] + 1
|
||||||
|
|
||||||
def add_user_category(self, label, name):
|
def add_user_category(self, label, name):
|
||||||
if label in self._tb_cats:
|
if label in self._tb_cats:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user