From f3b37944c4f0b87339118a9b4ab4d3ec86b2a711 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 7 Nov 2023 19:34:02 +0530 Subject: [PATCH] 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. --- src/calibre/gui2/__init__.py | 6 +++--- src/calibre/gui2/custom_column_widgets.py | 17 +++++++++-------- src/calibre/gui2/library/models.py | 8 ++++---- src/calibre/gui2/metadata/basic_widgets.py | 8 ++++++-- src/calibre/utils/date.py | 11 +++++++++++ 5 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 48a3d5a73d..fd574a1ff2 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -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'] diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index a5f9d949b9..1e628163f5 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -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) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 9ecc75372d..1ebe70b76d 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -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) diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 9d85133ce3..feba5a888e 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -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) diff --git a/src/calibre/utils/date.py b/src/calibre/utils/date.py index d9045d2b5f..c085b2df92 100644 --- a/src/calibre/utils/date.py +++ b/src/calibre/utils/date.py @@ -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: