diff --git a/Pipfile b/Pipfile index d92729d..63e3750 100644 --- a/Pipfile +++ b/Pipfile @@ -12,6 +12,8 @@ google-auth-httplib2 = "*" google-auth-oauthlib = "*" dateutil = "*" python-dateutil = "*" +pytz = "*" +tzlocal = "*" [requires] python_version = "3.7" diff --git a/bulk_reminders/api.py b/bulk_reminders/api.py index 6039d86..cd04ea9 100644 --- a/bulk_reminders/api.py +++ b/bulk_reminders/api.py @@ -3,12 +3,16 @@ from __future__ import print_function import datetime import os.path 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.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import Resource, build +from tzlocal import get_localzone # If modifying these scopes, delete the file token.json. SCOPES = ['https://www.googleapis.com/auth/calendar'] @@ -71,6 +75,7 @@ class Calendar(object): break 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 events = self.service.events().list(calendarId=calendarID, timeMin=now, maxResults=2500, singleEvents=True, @@ -82,6 +87,50 @@ class Calendar(object): return [(calendar['id'], calendar['summary']) for calendar in self.getCalendars()] -class Event(): - def __init__(self, title, date, time): - pass +class Event(object): + def __init__(self, summary: str, start: Union[datetime.date, datetime.datetime], end: Union[datetime.date, datetime.datetime], + 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))) diff --git a/bulk_reminders/gui.py b/bulk_reminders/gui.py index 09b491d..d4af1bb 100644 --- a/bulk_reminders/gui.py +++ b/bulk_reminders/gui.py @@ -4,6 +4,7 @@ from PyQt5.QtWidgets import QDialog, QMainWindow, QTableWidgetItem from dateutil.parser import isoparse from bulk_reminders import api, undo +from bulk_reminders.api import Event from bulk_reminders.gui_base import Ui_MainWindow from bulk_reminders.oauth import OAuthDialog @@ -45,8 +46,8 @@ class MainWindow(QMainWindow, Ui_MainWindow): header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents) self.eventsView.verticalHeader().hide() - self.undoButton.clicked().connect(self.undoEvents) - self.submitButton.clicked().connect(self.submitEvents) + self.undoButton.clicked.connect(self.undo) + self.submitButton.clicked.connect(self.submit) # Disable the undo button until undo stages are available if len(undo.stages) == 0: @@ -55,32 +56,25 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.populate() def undo(self) -> None: + # Get the latest undo stage and delete all events in that stage latest = undo.stages.pop(0) 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 if len(undo.stages) == 0: self.undoButton.setDisabled(True) + def submit(self) -> None: + pass + + def populate(self) -> None: """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)) for row, event in enumerate(self.events): - print(event) - 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))) - + event.fill_row(row, self.eventsView) @QtCore.pyqtSlot(int) def comboBoxChanged(self, row) -> None: diff --git a/bulk_reminders/undo.py b/bulk_reminders/undo.py new file mode 100644 index 0000000..16242c0 --- /dev/null +++ b/bulk_reminders/undo.py @@ -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()