Merge pull request #31772 from gavindsouza/refactor_google-calendar

refactor: Google Calendar
This commit is contained in:
gavin 2025-03-19 16:23:23 +01:00 committed by GitHub
commit 9a654347df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 235 additions and 199 deletions

View file

@ -20,6 +20,7 @@ from frappe.utils import (
date_diff, date_diff,
format_datetime, format_datetime,
get_datetime_str, get_datetime_str,
get_fullname,
getdate, getdate,
month_diff, month_diff,
now_datetime, now_datetime,
@ -38,6 +39,11 @@ communication_mapping = {
"Other": "Other", "Other": "Other",
} }
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.core.doctype.communication.communication import Communication
class Event(Document): class Event(Document):
# begin: auto-generated types # begin: auto-generated types
@ -107,42 +113,49 @@ class Event(Document):
def on_trash(self): def on_trash(self):
communications = frappe.get_all( communications = frappe.get_all(
"Communication", dict(reference_doctype=self.doctype, reference_name=self.name) "Communication",
filters={"reference_doctype": self.doctype, "reference_name": self.name},
pluck="name",
) )
if communications: for communication in communications:
for communication in communications: frappe.delete_doc("Communication", communication, force=True)
frappe.delete_doc_if_exists("Communication", communication.name, force=True)
def sync_communication(self): def sync_communication(self):
if self.event_participants: if not self.event_participants:
for participant in self.event_participants: return
filters = [
for participant in self.event_participants:
if communications := frappe.get_all(
"Communication",
filters=[
["Communication", "reference_doctype", "=", self.doctype], ["Communication", "reference_doctype", "=", self.doctype],
["Communication", "reference_name", "=", self.name], ["Communication", "reference_name", "=", self.name],
["Communication Link", "link_doctype", "=", participant.reference_doctype], ["Communication Link", "link_doctype", "=", participant.reference_doctype],
["Communication Link", "link_name", "=", participant.reference_docname], ["Communication Link", "link_name", "=", participant.reference_docname],
] ],
if comms := frappe.get_all("Communication", filters=filters, fields=["name"], distinct=True): pluck="name",
for comm in comms: distinct=True,
communication = frappe.get_doc("Communication", comm.name) ):
self.update_communication(participant, communication) for comm in communications:
else: communication = frappe.get_doc("Communication", comm)
meta = frappe.get_meta(participant.reference_doctype) self.update_communication(participant, communication)
if hasattr(meta, "allow_events_in_timeline") and meta.allow_events_in_timeline == 1: else:
self.create_communication(participant) meta = frappe.get_meta(participant.reference_doctype)
if hasattr(meta, "allow_events_in_timeline") and meta.allow_events_in_timeline == 1:
self.create_communication(participant)
def create_communication(self, participant): def create_communication(self, participant: "EventParticipants"):
communication = frappe.new_doc("Communication") communication = frappe.new_doc("Communication")
self.update_communication(participant, communication) self.update_communication(participant, communication)
self.communication = communication.name self.communication = communication.name
def update_communication(self, participant, communication): def update_communication(self, participant: "EventParticipants", communication: "Communication"):
communication.communication_medium = "Event" communication.communication_medium = "Event"
communication.subject = self.subject communication.subject = self.subject
communication.content = self.description if self.description else self.subject communication.content = self.description if self.description else self.subject
communication.communication_date = self.starts_on communication.communication_date = self.starts_on
communication.sender = self.owner communication.sender = self.owner
communication.sender_full_name = frappe.utils.get_fullname(self.owner) communication.sender_full_name = get_fullname(self.owner)
communication.reference_doctype = self.doctype communication.reference_doctype = self.doctype
communication.reference_name = self.name communication.reference_name = self.name
communication.communication_medium = ( communication.communication_medium = (
@ -195,28 +208,24 @@ class Event(Document):
@frappe.whitelist() @frappe.whitelist()
def delete_communication(event, reference_doctype, reference_docname): def delete_communication(event, reference_doctype, reference_docname):
deleted_participant = frappe.get_doc(reference_doctype, reference_docname)
if isinstance(event, str): if isinstance(event, str):
event = json.loads(event) event = json.loads(event)
filters = [ deleted_participant = frappe.get_doc(reference_doctype, reference_docname)
["Communication", "reference_doctype", "=", event.get("doctype")],
["Communication", "reference_name", "=", event.get("name")],
["Communication Link", "link_doctype", "=", deleted_participant.reference_doctype],
["Communication Link", "link_name", "=", deleted_participant.reference_docname],
]
comms = frappe.get_list("Communication", filters=filters, fields=["name"]) comms = frappe.get_list(
"Communication",
filters=[
["Communication", "reference_doctype", "=", event.get("doctype")],
["Communication", "reference_name", "=", event.get("name")],
["Communication Link", "link_doctype", "=", deleted_participant.reference_doctype],
["Communication Link", "link_name", "=", deleted_participant.reference_docname],
],
pluck="name",
)
if comms: for comm in comms:
deletion = [] frappe.delete_doc("Communication", comm)
for comm in comms:
delete = frappe.get_doc("Communication", comm.name).delete()
deletion.append(delete)
return deletion
return {}
def get_permission_query_conditions(user): def get_permission_query_conditions(user):

View file

@ -231,13 +231,13 @@ scheduler_events = {
"all": [ "all": [
"frappe.email.queue.flush", "frappe.email.queue.flush",
"frappe.monitor.flush", "frappe.monitor.flush",
"frappe.integrations.doctype.google_calendar.google_calendar.sync",
], ],
"hourly": [ "hourly": [
"frappe.model.utils.link_count.update_link_count", "frappe.model.utils.link_count.update_link_count",
"frappe.model.utils.user_settings.sync_user_settings", "frappe.model.utils.user_settings.sync_user_settings",
"frappe.desk.page.backups.backups.delete_downloadable_backups", "frappe.desk.page.backups.backups.delete_downloadable_backups",
"frappe.desk.form.document_follow.send_hourly_updates", "frappe.desk.form.document_follow.send_hourly_updates",
"frappe.integrations.doctype.google_calendar.google_calendar.sync",
"frappe.email.doctype.newsletter.newsletter.send_scheduled_email", "frappe.email.doctype.newsletter.newsletter.send_scheduled_email",
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.process_data_deletion_request", "frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.process_data_deletion_request",
], ],

View file

@ -2,8 +2,10 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE
from datetime import datetime, timedelta from contextlib import suppress
from urllib.parse import quote from datetime import date, datetime, timedelta
from math import ceil
from typing import TYPE_CHECKING, TypedDict
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import google.oauth2.credentials import google.oauth2.credentials
@ -13,7 +15,7 @@ from googleapiclient.discovery import build
from googleapiclient.errors import HttpError from googleapiclient.errors import HttpError
import frappe import frappe
from frappe import _ from frappe import _, _lt
from frappe.integrations.google_oauth import GoogleOAuth from frappe.integrations.google_oauth import GoogleOAuth
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import ( from frappe.utils import (
@ -27,6 +29,16 @@ from frappe.utils import (
) )
from frappe.utils.password import set_encrypted_password from frappe.utils.password import set_encrypted_password
if TYPE_CHECKING:
from frappe.desk.doctype.event.event import Event
class RecurrenceParameters(TypedDict):
frequency: str | None
until: datetime | None
byday: list[str]
SCOPES = "https://www.googleapis.com/auth/calendar" SCOPES = "https://www.googleapis.com/auth/calendar"
google_calendar_frequencies = { google_calendar_frequencies = {
@ -64,6 +76,9 @@ framework_days = {
} }
allow_google_calendar_label = _lt("Allow Google Calendar Access")
class GoogleCalendar(Document): class GoogleCalendar(Document):
# begin: auto-generated types # begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block. # This code is auto-generated. Do not modify anything in this block.
@ -86,7 +101,7 @@ class GoogleCalendar(Document):
# end: auto-generated types # end: auto-generated types
def validate(self): def validate(self):
google_settings = frappe.get_single("Google Settings") google_settings = frappe.get_cached_doc("Google Settings")
if not google_settings.enable: if not google_settings.enable:
frappe.throw(_("Enable Google API in Google Settings.")) frappe.throw(_("Enable Google API in Google Settings."))
@ -99,8 +114,9 @@ class GoogleCalendar(Document):
google_settings = self.validate() google_settings = self.validate()
if not self.refresh_token: if not self.refresh_token:
button_label = frappe.bold(_("Allow Google Calendar Access")) raise frappe.ValidationError(
raise frappe.ValidationError(_("Click on {0} to generate Refresh Token.").format(button_label)) _("Click on {0} to generate Refresh Token.").format(frappe.bold(allow_google_calendar_label))
)
data = { data = {
"client_id": google_settings.client_id, "client_id": google_settings.client_id,
@ -113,67 +129,64 @@ class GoogleCalendar(Document):
try: try:
r = requests.post(GoogleOAuth.OAUTH_URL, data=data).json() r = requests.post(GoogleOAuth.OAUTH_URL, data=data).json()
except requests.exceptions.HTTPError: except requests.exceptions.HTTPError:
button_label = frappe.bold(_("Allow Google Calendar Access"))
frappe.throw( frappe.throw(
_( _(
"Something went wrong during the token generation. Click on {0} to generate a new one." "Something went wrong during the token generation. Click on {0} to generate a new one."
).format(button_label) ).format(frappe.bold(allow_google_calendar_label))
) )
return r.get("access_token") return r.get("access_token")
@frappe.whitelist() @frappe.whitelist()
def authorize_access(g_calendar, reauthorize=None): def authorize_access(g_calendar: str, reauthorize: bool = False):
""" """
If no Authorization code get it from Google and then request for Refresh Token. If no Authorization code get it from Google and then request for Refresh Token.
Google Calendar Name is set to flags to set_value after Authorization Code is obtained. Google Calendar Name is set to flags to set_value after Authorization Code is obtained.
""" """
google_settings = frappe.get_doc("Google Settings")
google_calendar = frappe.get_doc("Google Calendar", g_calendar) google_calendar = frappe.get_doc("Google Calendar", g_calendar)
google_calendar.check_permission("write") google_calendar.check_permission("write")
google_settings = frappe.get_cached_doc("Google Settings")
redirect_uri = ( redirect_uri = (
get_request_site_address(True) f"{get_request_site_address(full_address=True)}"
+ "?cmd=frappe.integrations.doctype.google_calendar.google_calendar.google_callback" f"?cmd={google_callback.__module__}.{google_callback.__qualname__}"
) )
if not google_calendar.authorization_code or reauthorize: if not google_calendar.authorization_code or reauthorize:
frappe.cache.hset("google_calendar", "google_calendar", google_calendar.name) frappe.cache.hset("google_calendar", "google_calendar", google_calendar.name)
return get_authentication_url(client_id=google_settings.client_id, redirect_uri=redirect_uri) return get_authentication_url(client_id=google_settings.client_id, redirect_uri=redirect_uri)
else:
try:
data = {
"code": google_calendar.get_password(fieldname="authorization_code", raise_exception=False),
"client_id": google_settings.client_id,
"client_secret": google_settings.get_password(
fieldname="client_secret", raise_exception=False
),
"redirect_uri": redirect_uri,
"grant_type": "authorization_code",
}
r = requests.post(GoogleOAuth.OAUTH_URL, data=data).json()
if "refresh_token" in r: data = {
frappe.db.set_value( "code": google_calendar.get_password(fieldname="authorization_code", raise_exception=False),
"Google Calendar", google_calendar.name, "refresh_token", r.get("refresh_token") "client_id": google_settings.client_id,
) "client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False),
frappe.db.commit() "redirect_uri": redirect_uri,
"grant_type": "authorization_code",
}
frappe.local.response["type"] = "redirect" try:
frappe.local.response["location"] = "/app/Form/{}/{}".format( r = requests.post(GoogleOAuth.OAUTH_URL, data=data).json()
quote("Google Calendar"), quote(google_calendar.name) except Exception as e:
) frappe.throw(e)
frappe.msgprint(_("Google Calendar has been configured.")) if "refresh_token" in r:
except Exception as e: frappe.db.set_value("Google Calendar", google_calendar.name, "refresh_token", r["refresh_token"])
frappe.throw(e) frappe.db.commit()
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = google_calendar.get_url()
frappe.msgprint(_("Google Calendar has been configured."), indicator="green")
def get_authentication_url(client_id=None, redirect_uri=None): def get_authentication_url(client_id=None, redirect_uri=None):
return { return {
"url": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&prompt=consent&client_id={}&include_granted_scopes=true&scope={}&redirect_uri={}".format( "url": (
client_id, SCOPES, redirect_uri "https://accounts.google.com/o/oauth2/v2/auth?"
f"access_type=offline&response_type=code&prompt=consent&client_id={client_id}"
f"&include_granted_scopes=true&scope={SCOPES}&redirect_uri={redirect_uri}"
) )
} }
@ -191,69 +204,71 @@ def google_callback(code=None):
@frappe.whitelist() @frappe.whitelist()
def sync(g_calendar=None): def sync(g_calendar: str | None = None):
filters = {"enable": 1} filters = {"enable": 1, "pull_from_google_calendar": 1}
user_messages = []
if g_calendar: if g_calendar:
filters.update({"name": g_calendar}) filters.update({"name": g_calendar})
google_calendars = frappe.get_list("Google Calendar", filters=filters) for g in frappe.get_list("Google Calendar", filters=filters, pluck="name"):
user_messages.append(sync_events_from_google_calendar(g))
for g in google_calendars: return user_messages
return sync_events_from_google_calendar(g.name)
def get_google_calendar_object(g_calendar): def get_google_calendar_object(g_calendar):
"""Return an object of Google Calendar along with Google Calendar doc.""" """Return an object of Google Calendar along with Google Calendar doc."""
google_settings = frappe.get_doc("Google Settings") google_settings = frappe.get_cached_doc("Google Settings")
account = frappe.get_doc("Google Calendar", g_calendar) account: GoogleCalendar = frappe.get_doc("Google Calendar", g_calendar)
credentials_dict = { credentials = google.oauth2.credentials.Credentials(
"token": account.get_access_token(), token=account.get_access_token(),
"refresh_token": account.get_password(fieldname="refresh_token", raise_exception=False), refresh_token=account.get_password(fieldname="refresh_token", raise_exception=False),
"token_uri": GoogleOAuth.OAUTH_URL, token_uri=GoogleOAuth.OAUTH_URL,
"client_id": google_settings.client_id, client_id=google_settings.client_id,
"client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False), client_secret=google_settings.get_password(fieldname="client_secret", raise_exception=False),
"scopes": [SCOPES], scopes=[SCOPES],
} )
credentials = google.oauth2.credentials.Credentials(**credentials_dict)
google_calendar = build( google_calendar = build(
serviceName="calendar", version="v3", credentials=credentials, static_discovery=False serviceName="calendar", version="v3", credentials=credentials, static_discovery=False
) )
check_google_calendar(account, google_calendar) check_google_calendar(account.reload(), google_calendar)
account.load_from_db() return google_calendar, account.reload()
return google_calendar, account
def check_google_calendar(account, google_calendar): def check_google_calendar(account: GoogleCalendar, google_calendar):
""" """
Checks if Google Calendar is present with the specified name. Checks if Google Calendar is present with the specified name.
If not, creates one. If not, creates one.
""" """
account.load_from_db() if account.google_calendar_id:
try: try:
if account.google_calendar_id: return google_calendar.calendars().get(calendarId=account.google_calendar_id).execute()
google_calendar.calendars().get(calendarId=account.google_calendar_id).execute() except HttpError as err:
else: frappe.throw(
# If no Calendar ID create a new Calendar _("Google Calendar - Could not find Calendar for {0}, error code {1}.").format(
calendar = { account.name, err.resp.status
"summary": account.calendar_name, )
"timeZone": frappe.get_system_settings("time_zone"),
}
created_calendar = google_calendar.calendars().insert(body=calendar).execute()
frappe.db.set_value(
"Google Calendar", account.name, "google_calendar_id", created_calendar.get("id")
) )
frappe.db.commit()
# If no Calendar ID create a new Calendar
calendar = {
"summary": account.calendar_name,
"timeZone": frappe.get_system_settings("time_zone"),
}
try:
created_calendar = google_calendar.calendars().insert(body=calendar).execute()
except HttpError as err: except HttpError as err:
frappe.throw( frappe.throw(
_("Google Calendar - Could not create Calendar for {0}, error code {1}.").format( _("Google Calendar - Could not create Calendar for {0}, error code {1}.").format(
account.name, err.resp.status account.name, err.resp.status
) )
) )
account.db_set("google_calendar_id", created_calendar.get("id"))
frappe.db.commit()
def sync_events_from_google_calendar(g_calendar, method=None): def sync_events_from_google_calendar(g_calendar, method=None):
@ -308,30 +323,33 @@ def sync_events_from_google_calendar(g_calendar, method=None):
for idx, event in enumerate(results): for idx, event in enumerate(results):
frappe.publish_realtime( frappe.publish_realtime(
"import_google_calendar", dict(progress=idx + 1, total=len(results)), user=frappe.session.user "import_google_calendar", {"progress": idx + 1, "total": len(results)}, user=frappe.session.user
) )
# If Google Calendar Event if confirmed, then create an Event # If Google Calendar Event if confirmed, then create an Event
if event.get("status") == "confirmed": if event.get("status") == "confirmed":
recurrence = None recurrence = None
if event.get("recurrence"): if event.get("recurrence"):
try: with suppress(IndexError):
recurrence = event.get("recurrence")[0] recurrence = event.get("recurrence")[0]
except IndexError:
pass
if not frappe.db.exists("Event", {"google_calendar_event_id": event.get("id")}): if not frappe.db.exists("Event", {"google_calendar_event_id": event.get("id")}):
insert_event_to_calendar(account, event, recurrence) insert_event_to_calendar(account, event, recurrence)
else: else:
update_event_in_calendar(account, event, recurrence) update_event_in_calendar(account, event, recurrence)
# If any synced Google Calendar Event is cancelled, then close the Event
elif event.get("status") == "cancelled": elif event.get("status") == "cancelled":
# If any synced Google Calendar Event is cancelled, then close the Event event_name = frappe.db.get_value(
frappe.db.set_value(
"Event", "Event",
{ {
"google_calendar_id": account.google_calendar_id, "google_calendar_id": account.google_calendar_id,
"google_calendar_event_id": event.get("id"), "google_calendar_event_id": event.get("id"),
}, },
)
frappe.db.set_value(
"Event",
event_name,
"status", "status",
"Closed", "Closed",
) )
@ -340,19 +358,10 @@ def sync_events_from_google_calendar(g_calendar, method=None):
"doctype": "Comment", "doctype": "Comment",
"comment_type": "Info", "comment_type": "Info",
"reference_doctype": "Event", "reference_doctype": "Event",
"reference_name": frappe.db.get_value( "reference_name": event_name,
"Event",
{
"google_calendar_id": account.google_calendar_id,
"google_calendar_event_id": event.get("id"),
},
"name",
),
"content": " - Event deleted from Google Calendar.", "content": " - Event deleted from Google Calendar.",
} }
).insert(ignore_permissions=True) ).insert(ignore_permissions=True)
else:
pass
if not results: if not results:
return _("No Google Calendar Event to sync.") return _("No Google Calendar Event to sync.")
@ -368,7 +377,7 @@ def insert_event_to_calendar(account, event, recurrence=None):
""" """
calendar_event = { calendar_event = {
"doctype": "Event", "doctype": "Event",
"subject": event.get("summary"), "subject": event.get("summary") or "No Title",
"description": event.get("description"), "description": event.get("description"),
"google_calendar_event": 1, "google_calendar_event": 1,
"google_calendar": account.name, "google_calendar": account.name,
@ -376,13 +385,36 @@ def insert_event_to_calendar(account, event, recurrence=None):
"google_calendar_event_id": event.get("id"), "google_calendar_event_id": event.get("id"),
"google_meet_link": event.get("hangoutLink"), "google_meet_link": event.get("hangoutLink"),
"pulled_from_google_calendar": 1, "pulled_from_google_calendar": 1,
"owner": account.owner,
"event_type": "Public" if account.sync_as_public else "Private", "event_type": "Public" if account.sync_as_public else "Private",
} } | google_calendar_to_repeat_on(recurrence=recurrence, start=event.get("start"), end=event.get("end"))
calendar_event.update(
google_calendar_to_repeat_on(recurrence=recurrence, start=event.get("start"), end=event.get("end")) e: Event = frappe.get_doc(calendar_event)
update_participants_in_event(calendar_event=e, google_event=event)
e.insert(ignore_permissions=True)
e.db_set("owner", account.user, update_modified=False)
def update_participants_in_event(calendar_event: "Event", google_event: dict):
google_event_participants = [
attendee["email"] for attendee in google_event.get("attendees", []) if not attendee.get("self")
]
in_system_participants = frappe.get_all(
"User", filters={"email": ("in", google_event_participants)}, pluck="email"
) )
frappe.get_doc(calendar_event).insert(ignore_permissions=True)
existing_calendar_participants = [
participant.reference_docname
for participant in calendar_event.event_participants
if participant.reference_doctype == "User"
]
for participant in in_system_participants:
if participant not in existing_calendar_participants:
calendar_event.add_participant("User", participant)
# Add a Guest user to indicate participants not in the system
if len(in_system_participants) < len(google_event_participants):
calendar_event.add_participant("User", "Guest")
def update_event_in_calendar(account, event, recurrence=None): def update_event_in_calendar(account, event, recurrence=None):
@ -396,6 +428,7 @@ def update_event_in_calendar(account, event, recurrence=None):
calendar_event.update( calendar_event.update(
google_calendar_to_repeat_on(recurrence=recurrence, start=event.get("start"), end=event.get("end")) google_calendar_to_repeat_on(recurrence=recurrence, start=event.get("start"), end=event.get("end"))
) )
update_participants_in_event(calendar_event, event)
calendar_event.save(ignore_permissions=True) calendar_event.save(ignore_permissions=True)
@ -550,13 +583,10 @@ def delete_event_from_google_calendar(doc, method=None):
Delete Events from Google Calendar if Frappe Event is deleted. Delete Events from Google Calendar if Frappe Event is deleted.
""" """
if not frappe.db.exists("Google Calendar", {"name": doc.google_calendar}): if not frappe.db.exists("Google Calendar", {"name": doc.google_calendar, "push_to_google_calendar": 1}):
return return
google_calendar, account = get_google_calendar_object(doc.google_calendar) google_calendar, _ = get_google_calendar_object(doc.google_calendar)
if not account.push_to_google_calendar:
return
try: try:
event = ( event = (
@ -578,28 +608,23 @@ def delete_event_from_google_calendar(doc, method=None):
) )
def google_calendar_to_repeat_on(start, end, recurrence=None): def parse_google_calendar_date(dt):
if dt.get("date"):
return get_datetime(dt.get("date"))
return parser.parse(dt.get("dateTime")).astimezone(ZoneInfo(get_system_timezone())).replace(tzinfo=None)
def google_calendar_to_repeat_on(*, start, end, recurrence=None):
""" """
recurrence is in the form ['RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH'] recurrence is in the form ['RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH']
has the frequency and then the days on which the event recurs has the frequency and then the days on which the event recurs
Both have been mapped in a dict for easier mapping. Both have been mapped in a dict for easier mapping.
""" """
repeat_on = { repeat_on = {
"starts_on": ( "starts_on": parse_google_calendar_date(start),
get_datetime(start.get("date")) "ends_on": parse_google_calendar_date(end),
if start.get("date")
else parser.parse(start.get("dateTime"))
.astimezone(ZoneInfo(get_system_timezone()))
.replace(tzinfo=None)
),
"ends_on": (
get_datetime(end.get("date"))
if end.get("date")
else parser.parse(end.get("dateTime"))
.astimezone(ZoneInfo(get_system_timezone()))
.replace(tzinfo=None)
),
"all_day": 1 if start.get("date") else 0, "all_day": 1 if start.get("date") else 0,
"repeat_this_event": 1 if recurrence else 0, "repeat_this_event": 1 if recurrence else 0,
"repeat_on": None, "repeat_on": None,
@ -614,44 +639,45 @@ def google_calendar_to_repeat_on(start, end, recurrence=None):
} }
# recurrence rule "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH" # recurrence rule "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH"
if recurrence: if not recurrence:
# google_calendar_frequency = RRULE:FREQ=WEEKLY, byday = BYDAY=MO,TU,TH, until = 20191028 return repeat_on
google_calendar_frequency, until, byday = get_recurrence_parameters(recurrence)
repeat_on["repeat_on"] = google_calendar_frequencies.get(google_calendar_frequency)
if repeat_on["repeat_on"] == "Daily": # google_calendar_frequency = RRULE:FREQ=WEEKLY, byday = BYDAY=MO,TU,TH, until = 20191028
repeat_on["ends_on"] = None google_calendar_frequency, until, byday = get_recurrence_parameters(recurrence)
repeat_on["repeat_till"] = datetime.strptime(until, "%Y%m%d") if until else None repeat_on["repeat_on"] = google_calendar_frequencies.get(google_calendar_frequency)
if byday and repeat_on["repeat_on"] == "Weekly": if repeat_on["repeat_on"] == "Daily":
repeat_on["repeat_till"] = datetime.strptime(until, "%Y%m%d") if until else None repeat_on["ends_on"] = None
byday = byday.split("=")[1].split(",") repeat_on["repeat_till"] = until
for repeat_day in byday:
repeat_on[google_calendar_days[repeat_day]] = 1
if byday and repeat_on["repeat_on"] == "Monthly": if byday and repeat_on["repeat_on"] == "Weekly":
byday = byday.split("=")[1] repeat_on["repeat_till"] = until
repeat_day_week_number, repeat_day_name = None, None for repeat_day in byday:
repeat_on[google_calendar_days[repeat_day]] = 1
for num in ["-2", "-1", "1", "2", "3", "4", "5"]: if byday and repeat_on["repeat_on"] == "Monthly":
if num in byday: byday = byday.split("=")[1]
repeat_day_week_number = num repeat_day_week_number, repeat_day_name = None, None
break
for day in ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]: for num in ["-2", "-1", "1", "2", "3", "4", "5"]:
if day in byday: if num in byday:
repeat_day_name = google_calendar_days.get(day) repeat_day_week_number = num
break break
# Only Set starts_on for the event to repeat monthly for day in ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]:
start_date = parse_google_calendar_recurrence_rule(int(repeat_day_week_number), repeat_day_name) if day in byday:
repeat_on["starts_on"] = start_date repeat_day_name = google_calendar_days.get(day)
repeat_on["ends_on"] = add_to_date(start_date, minutes=5) break
repeat_on["repeat_till"] = datetime.strptime(until, "%Y%m%d") if until else None
if repeat_on["repeat_till"] == "Yearly": # Only Set starts_on for the event to repeat monthly
repeat_on["ends_on"] = None start_date = parse_google_calendar_recurrence_rule(int(repeat_day_week_number), repeat_day_name)
repeat_on["repeat_till"] = datetime.strptime(until, "%Y%m%d") if until else None repeat_on["starts_on"] = start_date
repeat_on["ends_on"] = add_to_date(start_date, minutes=5)
repeat_on["repeat_till"] = until
if repeat_on["repeat_till"] == "Yearly":
repeat_on["ends_on"] = None
repeat_on["repeat_till"] = until
return repeat_on return repeat_on
@ -725,13 +751,11 @@ def repeat_on_to_google_calendar_recurrence_rule(doc):
return [recurrence] return [recurrence]
def get_week_number(dt): def get_week_number(dt: date):
"""Return the week number of the month for the specified date. """Return the week number of the month for the specified date.
https://stackoverflow.com/questions/3806473/python-week-number-of-the-month/16804556 https://stackoverflow.com/questions/3806473/python-week-number-of-the-month/16804556
""" """
from math import ceil
first_day = dt.replace(day=1) first_day = dt.replace(day=1)
dom = dt.day dom = dt.day
@ -740,19 +764,21 @@ def get_week_number(dt):
return int(ceil(adjusted_dom / 7.0)) return int(ceil(adjusted_dom / 7.0))
def get_recurrence_parameters(recurrence): def get_recurrence_parameters(recurrence: str) -> RecurrenceParameters:
recurrence = recurrence.split(";") recurrence = recurrence.split(";")
frequency, until, byday = None, None, None frequency, until, byday = None, None, []
for r in recurrence: for token in recurrence:
if "RRULE:FREQ" in r: if "RRULE:FREQ" in token:
frequency = r frequency = token
elif "UNTIL" in r:
until = r elif "UNTIL" in token:
elif "BYDAY" in r: _until = token.replace("UNTIL=", "").rstrip("Z")
byday = r fmt = "%Y%m%dT%H%M%S" if "T" in _until else "%Y%m%d"
else: until = datetime.strptime(_until, fmt)
pass
elif "BYDAY" in token:
byday = token.split("=", 1)[1].split(",")
return frequency, until, byday return frequency, until, byday

View file

@ -28,7 +28,8 @@ class GoogleSettings(Document):
@frappe.whitelist() @frappe.whitelist()
def get_file_picker_settings(): def get_file_picker_settings():
"""Return all the data FileUploader needs to start the Google Drive Picker.""" """Return all the data FileUploader needs to start the Google Drive Picker."""
google_settings = frappe.get_single("Google Settings") google_settings = frappe.get_cached_doc("Google Settings")
if not (google_settings.enable and google_settings.google_drive_picker_enabled): if not (google_settings.enable and google_settings.google_drive_picker_enabled):
return {} return {}

View file

@ -125,7 +125,7 @@ class WebsiteSettings(Document):
) )
def validate_google_settings(self): def validate_google_settings(self):
if self.enable_google_indexing and not frappe.db.get_single_value("Google Settings", "enable"): if self.enable_google_indexing and not frappe.get_cached_value("Google Settings", None, "enable"):
frappe.throw(_("Enable Google API in Google Settings.")) frappe.throw(_("Enable Google API in Google Settings."))
def validate_redirects(self): def validate_redirects(self):