Fix editing dates with days sometimes off by one day. Fixes #2042815 [problems with dates](https://bugs.launchpad.net/calibre/+bug/2042815)

The problem seems to be some change in PyQt that causes auto-conversion
of python datetime objects to QDateTime to lose timezone information.
So we convert manually preserving that information.
This commit is contained in:
Kovid Goyal 2023-11-07 19:34:02 +05:30
parent 378e21b907
commit f3b37944c4
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 33 additions and 17 deletions

View File

@ -11,7 +11,7 @@ import threading
from contextlib import contextmanager, suppress from contextlib import contextmanager, suppress
from functools import lru_cache from functools import lru_cache
from qt.core import ( from qt.core import (
QApplication, QBuffer, QByteArray, QColor, QDateTime, QDesktopServices, QDialog, QApplication, QBuffer, QByteArray, QColor, QDesktopServices, QDialog,
QDialogButtonBox, QEvent, QFile, QFileDialog, QFileIconProvider, QFileInfo, QFont, QDialogButtonBox, QEvent, QFile, QFileDialog, QFileIconProvider, QFileInfo, QFont,
QFontDatabase, QFontInfo, QFontMetrics, QGuiApplication, QIcon, QImageReader, QFontDatabase, QFontInfo, QFontMetrics, QGuiApplication, QIcon, QImageReader,
QImageWriter, QIODevice, QLocale, QNetworkProxyFactory, QObject, QPalette, QImageWriter, QIODevice, QLocale, QNetworkProxyFactory, QObject, QPalette,
@ -37,7 +37,7 @@ from calibre.gui2.qt_file_dialogs import FileDialog
from calibre.ptempfile import base_dir from calibre.ptempfile import base_dir
from calibre.utils.config import Config, ConfigProxy, JSONConfig, dynamic from calibre.utils.config import Config, ConfigProxy, JSONConfig, dynamic
from calibre.utils.config_base import tweaks from calibre.utils.config_base import tweaks
from calibre.utils.date import UNDEFINED_DATE from calibre.utils.date import UNDEFINED_DATE, qt_from_dt
from calibre.utils.file_type_icons import EXT_MAP from calibre.utils.file_type_icons import EXT_MAP
from calibre.utils.img import set_image_allocation_limit from calibre.utils.img import set_image_allocation_limit
from calibre.utils.localization import get_lang from calibre.utils.localization import get_lang
@ -428,7 +428,7 @@ create_defs()
del create_defs del create_defs
# }}} # }}}
UNDEFINED_QDATETIME = QDateTime(UNDEFINED_DATE) UNDEFINED_QDATETIME = qt_from_dt(UNDEFINED_DATE, as_utc=True)
QT_HIDDEN_CLEAR_ACTION = '_q_qlineeditclearaction' QT_HIDDEN_CLEAR_ACTION = '_q_qlineeditclearaction'
ALL_COLUMNS = ['title', 'ondevice', 'authors', 'size', 'timestamp', 'rating', 'publisher', ALL_COLUMNS = ['title', 'ondevice', 'authors', 'size', 'timestamp', 'rating', 'publisher',
'tags', 'series', 'pubdate'] 'tags', 'series', 'pubdate']

View File

@ -9,7 +9,7 @@ import os
from collections import OrderedDict from collections import OrderedDict
from functools import partial from functools import partial
from qt.core import ( from qt.core import (
QApplication, QCheckBox, QComboBox, QDateTime, QDialog, QDoubleSpinBox, QGridLayout, QApplication, QCheckBox, QComboBox, QDialog, QDoubleSpinBox, QGridLayout,
QGroupBox, QHBoxLayout, QIcon, QLabel, QLineEdit, QMessageBox, QPlainTextEdit, QGroupBox, QHBoxLayout, QIcon, QLabel, QLineEdit, QMessageBox, QPlainTextEdit,
QSizePolicy, QSpacerItem, QSpinBox, QStyle, Qt, QToolButton, QUrl, QVBoxLayout, QSizePolicy, QSpacerItem, QSpinBox, QStyle, Qt, QToolButton, QUrl, QVBoxLayout,
QWidget, QWidget,
@ -18,15 +18,16 @@ from qt.core import (
from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata import title_sort
from calibre.gui2 import UNDEFINED_QDATETIME, elided_text, error_dialog, gprefs from calibre.gui2 import UNDEFINED_QDATETIME, elided_text, error_dialog, gprefs
from calibre.gui2.comments_editor import Editor as CommentsEditor from calibre.gui2.comments_editor import Editor as CommentsEditor
from calibre.gui2.markdown_editor import Editor as MarkdownEditor
from calibre.gui2.complete2 import EditWithComplete as EWC from calibre.gui2.complete2 import EditWithComplete as EWC
from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.gui2.library.delegates import ClearingDoubleSpinBox, ClearingSpinBox from calibre.gui2.library.delegates import ClearingDoubleSpinBox, ClearingSpinBox
from calibre.gui2.markdown_editor import Editor as MarkdownEditor
from calibre.gui2.widgets2 import DateTimeEdit as DateTimeEditBase, RatingEditor from calibre.gui2.widgets2 import DateTimeEdit as DateTimeEditBase, RatingEditor
from calibre.library.comments import comments_to_html from calibre.library.comments import comments_to_html
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre.utils.date import ( from calibre.utils.date import (
as_local_time, as_utc, internal_iso_format_string, is_date_undefined, now, qt_to_dt, as_local_time, as_utc, internal_iso_format_string, is_date_undefined, now,
qt_from_dt, qt_to_dt,
) )
from calibre.utils.icu import lower as icu_lower, sort_key from calibre.utils.icu import lower as icu_lower, sort_key
@ -349,7 +350,7 @@ class DateTimeEdit(DateTimeEditBase):
DateTimeEditBase.focusOutEvent(self, x) DateTimeEditBase.focusOutEvent(self, x)
def set_to_today(self): def set_to_today(self):
self.setDateTime(now()) self.setDateTime(qt_from_dt(now()))
def set_to_clear(self): def set_to_clear(self):
self.setDateTime(UNDEFINED_QDATETIME) self.setDateTime(UNDEFINED_QDATETIME)
@ -400,12 +401,12 @@ class DateTime(Base):
if val is None: if val is None:
val = self.dte.minimumDateTime() val = self.dte.minimumDateTime()
else: else:
val = QDateTime(val) val = qt_from_dt(val)
self.dte.setDateTime(val) self.dte.setDateTime(val)
def getter(self): def getter(self):
val = self.dte.dateTime() val = self.dte.dateTime()
if val <= UNDEFINED_QDATETIME: if is_date_undefined(val):
val = None val = None
else: else:
val = qt_to_dt(val) val = qt_to_dt(val)
@ -1251,13 +1252,13 @@ class BulkDateTime(BulkBase):
if val is None: if val is None:
val = self.main_widget.minimumDateTime() val = self.main_widget.minimumDateTime()
else: else:
val = QDateTime(val) val = qt_from_dt(val)
self.main_widget.setDateTime(val) self.main_widget.setDateTime(val)
self.ignore_change_signals = False self.ignore_change_signals = False
def getter(self): def getter(self):
val = self.main_widget.dateTime() val = self.main_widget.dateTime()
if val <= UNDEFINED_QDATETIME: if is_date_undefined(val):
val = None val = None
else: else:
val = qt_to_dt(val) val = qt_to_dt(val)

View File

@ -15,8 +15,8 @@ import traceback
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from itertools import groupby from itertools import groupby
from qt.core import ( from qt.core import (
QAbstractTableModel, QApplication, QColor, QDateTime, QFont, QFontMetrics, QIcon, QAbstractTableModel, QApplication, QColor, QFont, QFontMetrics, QIcon, QImage,
QImage, QModelIndex, QPainter, QPixmap, Qt, pyqtSignal, QModelIndex, QPainter, QPixmap, Qt, pyqtSignal,
) )
from calibre import ( from calibre import (
@ -34,7 +34,7 @@ from calibre.library.save_to_disk import find_plugboard
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import device_prefs, prefs, tweaks from calibre.utils.config import device_prefs, prefs, tweaks
from calibre.utils.date import ( from calibre.utils.date import (
UNDEFINED_DATE, as_local_time, dt_factory, is_date_undefined, qt_to_dt, UNDEFINED_DATE, dt_factory, is_date_undefined, qt_from_dt, qt_to_dt,
) )
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.utils.localization import calibre_langcode_to_name, ngettext from calibre.utils.localization import calibre_langcode_to_name, ngettext
@ -919,7 +919,7 @@ class BooksModel(QAbstractTableModel): # {{{
elif dt == 'datetime': elif dt == 'datetime':
def func(idx): def func(idx):
val = fffunc(field_obj, idfunc(idx), default_value=UNDEFINED_DATE) val = fffunc(field_obj, idfunc(idx), default_value=UNDEFINED_DATE)
return None if is_date_undefined(val) else QDateTime(as_local_time(val)) return None if is_date_undefined(val) else qt_from_dt(val)
elif dt == 'rating': elif dt == 'rating':
rating_fields[field] = m['display'].get('allow_half_stars', False) rating_fields[field] = m['display'].get('allow_half_stars', False)

View File

@ -36,7 +36,9 @@ from calibre.gui2.comments_editor import Editor
from calibre.gui2.complete2 import EditWithComplete from calibre.gui2.complete2 import EditWithComplete
from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.gui2.languages import LanguagesEdit as LE from calibre.gui2.languages import LanguagesEdit as LE
from calibre.gui2.widgets import EnLineEdit, FormatList as _FormatList, ImageView, LineEditIndicators from calibre.gui2.widgets import (
EnLineEdit, FormatList as _FormatList, ImageView, LineEditIndicators,
)
from calibre.gui2.widgets2 import ( from calibre.gui2.widgets2 import (
DateTimeEdit, Dialog, RatingEditor, RightClickButton, access_key, DateTimeEdit, Dialog, RatingEditor, RightClickButton, access_key,
populate_standard_spinbox_context_menu, populate_standard_spinbox_context_menu,
@ -46,7 +48,7 @@ from calibre.ptempfile import PersistentTemporaryFile, SpooledTemporaryFile
from calibre.utils.config import prefs, tweaks from calibre.utils.config import prefs, tweaks
from calibre.utils.date import ( from calibre.utils.date import (
UNDEFINED_DATE, as_local_time, internal_iso_format_string, is_date_undefined, UNDEFINED_DATE, as_local_time, internal_iso_format_string, is_date_undefined,
local_tz, parse_only_date, qt_to_dt, utcfromtimestamp, local_tz, parse_only_date, qt_from_dt, qt_to_dt, utcfromtimestamp,
) )
from calibre.utils.filenames import make_long_path_useable from calibre.utils.filenames import make_long_path_useable
from calibre.utils.icu import sort_key, strcmp from calibre.utils.icu import sort_key, strcmp
@ -188,6 +190,8 @@ def make_undoable(spinbox):
if hasattr(self, 'setDateTime'): if hasattr(self, 'setDateTime'):
if isinstance(val, date) and not isinstance(val, datetime) and not is_date_undefined(val): if isinstance(val, date) and not isinstance(val, datetime) and not is_date_undefined(val):
val = parse_only_date(val.isoformat(), assume_utc=False, as_utc=False) val = parse_only_date(val.isoformat(), assume_utc=False, as_utc=False)
if isinstance(val, datetime):
val = qt_from_dt(val)
self.setDateTime(val) self.setDateTime(val)
elif hasattr(self, 'setValue'): elif hasattr(self, 'setValue'):
self.setValue(val) self.setValue(val)

View File

@ -178,6 +178,17 @@ def qt_to_dt(qdate_or_qdatetime, as_utc=True):
return dt.astimezone(_utc_tz if as_utc else _local_tz) return dt.astimezone(_utc_tz if as_utc else _local_tz)
def qt_from_dt(d, as_utc=False, assume_utc=False):
from qt.core import QDateTime, QTimeZone
if d.tzinfo is None:
d = d.replace(tzinfo=utc_tz if assume_utc else local_tz)
d = d.astimezone(utc_tz)
ans = QDateTime.fromMSecsSinceEpoch(int(d.timestamp() * 1000), QTimeZone.utc())
if not as_utc:
ans = ans.toLocalTime()
return ans
def fromtimestamp(ctime, as_utc=True): def fromtimestamp(ctime, as_utc=True):
dt = datetime.utcfromtimestamp(ctime).replace(tzinfo=_utc_tz) dt = datetime.utcfromtimestamp(ctime).replace(tzinfo=_utc_tz)
if not as_utc: if not as_utc: