Merge pull request #31772 from gavindsouza/refactor_google-calendar
refactor: Google Calendar
This commit is contained in:
commit
9a654347df
5 changed files with 235 additions and 199 deletions
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue