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 functools import lru_cache
from qt.core import (
QApplication, QBuffer, QByteArray, QColor, QDateTime, QDesktopServices, QDialog,
QApplication, QBuffer, QByteArray, QColor, QDesktopServices, QDialog,
QDialogButtonBox, QEvent, QFile, QFileDialog, QFileIconProvider, QFileInfo, QFont,
QFontDatabase, QFontInfo, QFontMetrics, QGuiApplication, QIcon, QImageReader,
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.utils.config import Config, ConfigProxy, JSONConfig, dynamic
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.img import set_image_allocation_limit
from calibre.utils.localization import get_lang
@ -428,7 +428,7 @@ 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'
ALL_COLUMNS = ['title', 'ondevice', 'authors', 'size', 'timestamp', 'rating', 'publisher',
'tags', 'series', 'pubdate']

View File

@ -9,7 +9,7 @@ import os
from collections import OrderedDict
from functools import partial
from qt.core import (
QApplication, QCheckBox, QComboBox, QDateTime, QDialog, QDoubleSpinBox, QGridLayout,
QApplication, QCheckBox, QComboBox, QDialog, QDoubleSpinBox, QGridLayout,
QGroupBox, QHBoxLayout, QIcon, QLabel, QLineEdit, QMessageBox, QPlainTextEdit,
QSizePolicy, QSpacerItem, QSpinBox, QStyle, Qt, QToolButton, QUrl, QVBoxLayout,
QWidget,
@ -18,15 +18,16 @@ from qt.core import (
from calibre.ebooks.metadata import title_sort
from calibre.gui2 import UNDEFINED_QDATETIME, elided_text, error_dialog, gprefs
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.dialogs.tag_editor import TagEditor
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.library.comments import comments_to_html
from calibre.utils.config import tweaks
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
@ -349,7 +350,7 @@ class DateTimeEdit(DateTimeEditBase):
DateTimeEditBase.focusOutEvent(self, x)
def set_to_today(self):
self.setDateTime(now())
self.setDateTime(qt_from_dt(now()))
def set_to_clear(self):
self.setDateTime(UNDEFINED_QDATETIME)
@ -400,12 +401,12 @@ class DateTime(Base):
if val is None:
val = self.dte.minimumDateTime()
else:
val = QDateTime(val)
val = qt_from_dt(val)
self.dte.setDateTime(val)
def getter(self):
val = self.dte.dateTime()
if val <= UNDEFINED_QDATETIME:
if is_date_undefined(val):
val = None
else:
val = qt_to_dt(val)
@ -1251,13 +1252,13 @@ class BulkDateTime(BulkBase):
if val is None:
val = self.main_widget.minimumDateTime()
else:
val = QDateTime(val)
val = qt_from_dt(val)
self.main_widget.setDateTime(val)
self.ignore_change_signals = False
def getter(self):
val = self.main_widget.dateTime()
if val <= UNDEFINED_QDATETIME:
if is_date_undefined(val):
val = None
else:
val = qt_to_dt(val)

View File

@ -15,8 +15,8 @@ import traceback
from collections import defaultdict, namedtuple
from itertools import groupby
from qt.core import (
QAbstractTableModel, QApplication, QColor, QDateTime, QFont, QFontMetrics, QIcon,
QImage, QModelIndex, QPainter, QPixmap, Qt, pyqtSignal,
QAbstractTableModel, QApplication, QColor, QFont, QFontMetrics, QIcon, QImage,
QModelIndex, QPainter, QPixmap, Qt, pyqtSignal,
)
from calibre import (
@ -34,7 +34,7 @@ from calibre.library.save_to_disk import find_plugboard
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import device_prefs, prefs, tweaks
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.localization import calibre_langcode_to_name, ngettext
@ -919,7 +919,7 @@ class BooksModel(QAbstractTableModel): # {{{
elif dt == 'datetime':
def func(idx):
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':
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.dialogs.tag_editor import TagEditor
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 (
DateTimeEdit, Dialog, RatingEditor, RightClickButton, access_key,
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.date import (
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.icu import sort_key, strcmp
@ -188,6 +190,8 @@ def make_undoable(spinbox):
if hasattr(self, 'setDateTime'):
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)
if isinstance(val, datetime):
val = qt_from_dt(val)
self.setDateTime(val)
elif hasattr(self, 'setValue'):
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)
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):
dt = datetime.utcfromtimestamp(ctime).replace(tzinfo=_utc_tz)
if not as_utc: