mirror of
https://github.com/Xevion/bulk-reminders.git
synced 2025-12-06 13:14:33 -06:00
Begin implementing staged undo system
This commit is contained in:
2
Pipfile
2
Pipfile
@@ -12,6 +12,8 @@ google-auth-httplib2 = "*"
|
|||||||
google-auth-oauthlib = "*"
|
google-auth-oauthlib = "*"
|
||||||
dateutil = "*"
|
dateutil = "*"
|
||||||
python-dateutil = "*"
|
python-dateutil = "*"
|
||||||
|
pytz = "*"
|
||||||
|
tzlocal = "*"
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.7"
|
python_version = "3.7"
|
||||||
|
|||||||
@@ -3,12 +3,16 @@ from __future__ import print_function
|
|||||||
import datetime
|
import datetime
|
||||||
import os.path
|
import os.path
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Any, Iterator, List, Optional, Tuple
|
from typing import Any, Iterator, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtGui
|
||||||
|
from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem
|
||||||
|
from dateutil.parser import isoparse
|
||||||
from google.auth.transport.requests import Request
|
from google.auth.transport.requests import Request
|
||||||
from google.oauth2.credentials import Credentials
|
from google.oauth2.credentials import Credentials
|
||||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||||
from googleapiclient.discovery import Resource, build
|
from googleapiclient.discovery import Resource, build
|
||||||
|
from tzlocal import get_localzone
|
||||||
|
|
||||||
# If modifying these scopes, delete the file token.json.
|
# If modifying these scopes, delete the file token.json.
|
||||||
SCOPES = ['https://www.googleapis.com/auth/calendar']
|
SCOPES = ['https://www.googleapis.com/auth/calendar']
|
||||||
@@ -71,6 +75,7 @@ class Calendar(object):
|
|||||||
break
|
break
|
||||||
|
|
||||||
def getEvents(self, calendarID: str) -> List[Any]:
|
def getEvents(self, calendarID: str) -> List[Any]:
|
||||||
|
"""Retrieves up to 2500 events for a given calendar ordered by occurrence that happen in the future."""
|
||||||
now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
|
now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
|
||||||
events = self.service.events().list(calendarId=calendarID, timeMin=now,
|
events = self.service.events().list(calendarId=calendarID, timeMin=now,
|
||||||
maxResults=2500, singleEvents=True,
|
maxResults=2500, singleEvents=True,
|
||||||
@@ -82,6 +87,50 @@ class Calendar(object):
|
|||||||
return [(calendar['id'], calendar['summary']) for calendar in self.getCalendars()]
|
return [(calendar['id'], calendar['summary']) for calendar in self.getCalendars()]
|
||||||
|
|
||||||
|
|
||||||
class Event():
|
class Event(object):
|
||||||
def __init__(self, title, date, time):
|
def __init__(self, summary: str, start: Union[datetime.date, datetime.datetime], end: Union[datetime.date, datetime.datetime],
|
||||||
pass
|
description: Optional[str] = None):
|
||||||
|
if type(start) != type(end):
|
||||||
|
raise Exception("Both start and end times need to be either simple dates or advanced datetime objects.")
|
||||||
|
self.summary, self.start, self.end, self.description = summary, start, end, description
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_api(cls, event: dict) -> 'Event':
|
||||||
|
"""Returns a Event object from a Google API Engine item."""
|
||||||
|
print(event)
|
||||||
|
return Event(summary=event.get('summary'),
|
||||||
|
start=isoparse(event['start'].get('dateTime', event['start'].get('date'))),
|
||||||
|
end=isoparse(event['end'].get('dateTime', event['end'].get('date'))),
|
||||||
|
description=event.get('description'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_datetime(self) -> bool:
|
||||||
|
"""Returns true if the Event object is based on full datetime objects instead of simple date objects."""
|
||||||
|
return type(self.start) is datetime.datetime
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_start(self) -> dict:
|
||||||
|
"""Provides a proper object for the 'start' field in the body of a new event."""
|
||||||
|
if type(self.start) is datetime.date:
|
||||||
|
return {'date': self.start.strftime('%Y-%m-%d')}
|
||||||
|
elif type(self.start) is datetime.datetime:
|
||||||
|
return {'datetime': self.start.astimezone(get_localzone()).isoformat()}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_end(self) -> dict:
|
||||||
|
"""Provides a proper object for the 'end' field in the body of a new event."""
|
||||||
|
if type(self.end) is datetime.date:
|
||||||
|
return {'date': self.end.strftime('%Y-%m-%d')}
|
||||||
|
elif type(self.end) is datetime.datetime:
|
||||||
|
return {'datetime': self.end.astimezone(get_localzone()).isoformat()}
|
||||||
|
|
||||||
|
def fill_row(self, row: int, table: QTableWidget) -> None:
|
||||||
|
"""Fills a specific row on a QTableWidget object with the information stored in the Event object."""
|
||||||
|
summaryItem = QTableWidgetItem(self.summary)
|
||||||
|
summaryItem.setForeground(QtGui.QColor("blue"))
|
||||||
|
table.setItem(row, 0, summaryItem)
|
||||||
|
|
||||||
|
formatString = '%b %d, %Y' if self.start is not None else '%b %d, %Y %I:%M %Z'
|
||||||
|
table.setItem(row, 1, QTableWidgetItem('Foreign'))
|
||||||
|
table.setItem(row, 2, QTableWidgetItem(self.start.strftime(formatString)))
|
||||||
|
table.setItem(row, 3, QTableWidgetItem(self.end.strftime(formatString)))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from PyQt5.QtWidgets import QDialog, QMainWindow, QTableWidgetItem
|
|||||||
from dateutil.parser import isoparse
|
from dateutil.parser import isoparse
|
||||||
|
|
||||||
from bulk_reminders import api, undo
|
from bulk_reminders import api, undo
|
||||||
|
from bulk_reminders.api import Event
|
||||||
from bulk_reminders.gui_base import Ui_MainWindow
|
from bulk_reminders.gui_base import Ui_MainWindow
|
||||||
from bulk_reminders.oauth import OAuthDialog
|
from bulk_reminders.oauth import OAuthDialog
|
||||||
|
|
||||||
@@ -45,8 +46,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
|
header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
|
||||||
self.eventsView.verticalHeader().hide()
|
self.eventsView.verticalHeader().hide()
|
||||||
|
|
||||||
self.undoButton.clicked().connect(self.undoEvents)
|
self.undoButton.clicked.connect(self.undo)
|
||||||
self.submitButton.clicked().connect(self.submitEvents)
|
self.submitButton.clicked.connect(self.submit)
|
||||||
|
|
||||||
# Disable the undo button until undo stages are available
|
# Disable the undo button until undo stages are available
|
||||||
if len(undo.stages) == 0:
|
if len(undo.stages) == 0:
|
||||||
@@ -55,32 +56,25 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.populate()
|
self.populate()
|
||||||
|
|
||||||
def undo(self) -> None:
|
def undo(self) -> None:
|
||||||
|
# Get the latest undo stage and delete all events in that stage
|
||||||
latest = undo.stages.pop(0)
|
latest = undo.stages.pop(0)
|
||||||
for entry in latest:
|
for entry in latest:
|
||||||
|
self.calendar.service.events().delete(calendarId=entry.get('calendarId'), eventId=entry.get('eventId')).execute()
|
||||||
|
|
||||||
# Disable the undo button until undo stages are available
|
# Disable the undo button until undo stages are available
|
||||||
if len(undo.stages) == 0:
|
if len(undo.stages) == 0:
|
||||||
self.undoButton.setDisabled(True)
|
self.undoButton.setDisabled(True)
|
||||||
|
|
||||||
|
def submit(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def populate(self) -> None:
|
def populate(self) -> None:
|
||||||
"""Re-populate the table with all of the events"""
|
"""Re-populate the table with all of the events"""
|
||||||
self.events = self.calendar.getEvents(self.currentCalendarID)
|
self.events = [Event.from_api(event) for event in self.calendar.getEvents(self.currentCalendarID)]
|
||||||
self.eventsView.setRowCount(len(self.events))
|
self.eventsView.setRowCount(len(self.events))
|
||||||
for row, event in enumerate(self.events):
|
for row, event in enumerate(self.events):
|
||||||
print(event)
|
event.fill_row(row, self.eventsView)
|
||||||
summaryItem = QTableWidgetItem(event['summary'])
|
|
||||||
summaryItem.setData(QtCore.Qt.UserRole)
|
|
||||||
summaryItem.setForeground(QtGui.QColor("blue"))
|
|
||||||
self.eventsView.setItem(row, 0, summaryItem)
|
|
||||||
start, end = event['start'], event['end']
|
|
||||||
start, end = start.get('date') or start.get('dateTime'), end.get('date') or end.get('dateTime')
|
|
||||||
start, end = isoparse(start), isoparse(end)
|
|
||||||
|
|
||||||
formatString = '%b %d, %Y'if event['start'].get('date') is not None else '%b %d, %Y %I:%M %Z'
|
|
||||||
self.eventsView.setItem(row, 1, QTableWidgetItem('Foreign'))
|
|
||||||
self.eventsView.setItem(row, 2, QTableWidgetItem(start.strftime(formatString)))
|
|
||||||
self.eventsView.setItem(row, 3, QTableWidgetItem(end.strftime(formatString)))
|
|
||||||
|
|
||||||
|
|
||||||
@QtCore.pyqtSlot(int)
|
@QtCore.pyqtSlot(int)
|
||||||
def comboBoxChanged(self, row) -> None:
|
def comboBoxChanged(self, row) -> None:
|
||||||
|
|||||||
20
bulk_reminders/undo.py
Normal file
20
bulk_reminders/undo.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
stages = []
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> None:
|
||||||
|
"""Load data from the undo history file"""
|
||||||
|
with open('history.json', 'r') as history:
|
||||||
|
stages = json.load(history)
|
||||||
|
|
||||||
|
|
||||||
|
def save() -> None:
|
||||||
|
"""Save data to the undo history file."""
|
||||||
|
with open('history.json', 'w') as history:
|
||||||
|
stages = json.dump(history)
|
||||||
|
|
||||||
|
|
||||||
|
if os.path.exists('history.json'):
|
||||||
|
load()
|
||||||
Reference in New Issue
Block a user