mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-04-24 13:40:25 -05:00
fix: Improve CalDAV integration error handling and feature completeness
Fix multiple issues with CalDAV calendar integration: Error Handling & User Experience: - Fix 'Integration not found' error after saving CalDAV configuration - Improve error handling in view_integration route to gracefully handle missing/invalid credentials - Add connector error display in integration view template with helpful links - Fix 'Test Connection' button always showing errors by improving credential validation - Add better error messages for SSL, timeout, and connection errors with actionable guidance Sync Improvements: - Fix sync returning 0 items with no feedback - now provides detailed messages - Add support for DURATION-based events (not just DTEND) - Improve sync result messages to distinguish between 'no events found' vs 'all events already imported' - Add synced_items field for compatibility with scheduled sync tasks - Better handling of duplicate UID race conditions during import CalDAV Client Enhancements: - Improve XML parsing error handling with clear error messages - Handle empty responses gracefully (return empty multistatus for no events) - Add time range validation in fetch_events - Better error handling in calendar discovery with step-by-step error messages - Improve iCalendar parsing with better error logging Configuration & Setup: - Add auto_sync configuration option in CalDAV setup form - Add URL validation for server_url and calendar_url - Ensure credentials are properly saved and integration marked as active - Improve CalDAV setup route to handle missing credentials during initial setup Logging & Debugging: - Add comprehensive logging throughout sync process - Log calendar discovery steps and event fetch operations - Better error context in logs for troubleshooting This addresses user-reported issues where: - Users couldn't click on integration after saving - Test Connection always failed with generic errors - Sync showed 0 items with no explanation - Credentials weren't being saved properly
This commit is contained in:
@@ -96,16 +96,23 @@ class CalDAVClient:
|
||||
}
|
||||
if headers:
|
||||
h.update(headers)
|
||||
resp = requests.request(
|
||||
method,
|
||||
url,
|
||||
headers=h,
|
||||
data=data.encode("utf-8") if isinstance(data, str) else data,
|
||||
auth=(self.username, self.password),
|
||||
verify=self.verify_ssl,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
return resp
|
||||
try:
|
||||
resp = requests.request(
|
||||
method,
|
||||
url,
|
||||
headers=h,
|
||||
data=data.encode("utf-8") if isinstance(data, str) else data,
|
||||
auth=(self.username, self.password),
|
||||
verify=self.verify_ssl,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
return resp
|
||||
except requests.exceptions.SSLError as e:
|
||||
raise ValueError(f"SSL certificate verification failed. If using a self-signed certificate, disable SSL verification in settings. Error: {str(e)}") from e
|
||||
except requests.exceptions.Timeout as e:
|
||||
raise ValueError(f"Request timeout after {self.timeout} seconds. The server may be slow or unreachable.") from e
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
raise ValueError(f"Connection error: {str(e)}. Please check the server URL and network connectivity.") from e
|
||||
|
||||
def _propfind(self, url: str, xml_body: str, depth: str = "0") -> ET.Element:
|
||||
resp = self._request(
|
||||
@@ -115,7 +122,12 @@ class CalDAVClient:
|
||||
data=xml_body,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return ET.fromstring(resp.text)
|
||||
if not resp.text or not resp.text.strip():
|
||||
raise ValueError(f"Empty response from PROPFIND request to {url}")
|
||||
try:
|
||||
return ET.fromstring(resp.text)
|
||||
except ET.ParseError as e:
|
||||
raise ValueError(f"Invalid XML response from server: {str(e)}") from e
|
||||
|
||||
def _report(self, url: str, xml_body: str, depth: str = "1") -> ET.Element:
|
||||
resp = self._request(
|
||||
@@ -125,7 +137,13 @@ class CalDAVClient:
|
||||
data=xml_body,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return ET.fromstring(resp.text)
|
||||
if not resp.text or not resp.text.strip():
|
||||
# Empty response might mean no events found, return empty multistatus
|
||||
return ET.Element(_ns("multistatus", DAV_NS))
|
||||
try:
|
||||
return ET.fromstring(resp.text)
|
||||
except ET.ParseError as e:
|
||||
raise ValueError(f"Invalid XML response from server: {str(e)}") from e
|
||||
|
||||
def discover_calendars(self, server_url: str) -> List[CalDAVCalendar]:
|
||||
"""
|
||||
@@ -137,31 +155,39 @@ class CalDAVClient:
|
||||
server_url = _ensure_trailing_slash(server_url)
|
||||
|
||||
# 1) Find current-user-principal
|
||||
body = (
|
||||
'<?xml version="1.0" encoding="utf-8" ?>'
|
||||
f'<d:propfind xmlns:d="{DAV_NS}">'
|
||||
"<d:prop><d:current-user-principal/></d:prop>"
|
||||
"</d:propfind>"
|
||||
)
|
||||
root = self._propfind(server_url, body, depth="0")
|
||||
principal_href = self._find_href(root, [(_ns("current-user-principal", DAV_NS),)])
|
||||
if not principal_href:
|
||||
# Some servers support well-known principal path fallback
|
||||
principal_href = "/.well-known/caldav"
|
||||
try:
|
||||
body = (
|
||||
'<?xml version="1.0" encoding="utf-8" ?>'
|
||||
f'<d:propfind xmlns:d="{DAV_NS}">'
|
||||
"<d:prop><d:current-user-principal/></d:prop>"
|
||||
"</d:propfind>"
|
||||
)
|
||||
root = self._propfind(server_url, body, depth="0")
|
||||
principal_href = self._find_href(root, [(_ns("current-user-principal", DAV_NS),)])
|
||||
if not principal_href:
|
||||
# Some servers support well-known principal path fallback
|
||||
principal_href = "/.well-known/caldav"
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to discover current-user-principal from {server_url}: {str(e)}") from e
|
||||
|
||||
principal_url = urljoin(server_url, principal_href)
|
||||
|
||||
# 2) Find calendar-home-set on principal
|
||||
body = (
|
||||
'<?xml version="1.0" encoding="utf-8" ?>'
|
||||
f'<d:propfind xmlns:d="{DAV_NS}" xmlns:cs="{CALDAV_NS}">'
|
||||
"<d:prop><cs:calendar-home-set/></d:prop>"
|
||||
"</d:propfind>"
|
||||
)
|
||||
root = self._propfind(principal_url, body, depth="0")
|
||||
home_href = self._find_href(root, [(_ns("calendar-home-set", CALDAV_NS),)])
|
||||
if not home_href:
|
||||
raise ValueError("Could not discover calendar-home-set from CalDAV server.")
|
||||
try:
|
||||
body = (
|
||||
'<?xml version="1.0" encoding="utf-8" ?>'
|
||||
f'<d:propfind xmlns:d="{DAV_NS}" xmlns:cs="{CALDAV_NS}">'
|
||||
"<d:prop><cs:calendar-home-set/></d:prop>"
|
||||
"</d:propfind>"
|
||||
)
|
||||
root = self._propfind(principal_url, body, depth="0")
|
||||
home_href = self._find_href(root, [(_ns("calendar-home-set", CALDAV_NS),)])
|
||||
if not home_href:
|
||||
raise ValueError("Could not discover calendar-home-set from CalDAV server. The server may not support CalDAV or the credentials may be incorrect.")
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to discover calendar-home-set from {principal_url}: {str(e)}") from e
|
||||
|
||||
home_url = urljoin(server_url, home_href)
|
||||
|
||||
@@ -207,8 +233,15 @@ class CalDAVClient:
|
||||
"""
|
||||
Fetch VEVENTs within a time range using a calendar-query REPORT.
|
||||
Returns a list of dicts with uid, summary, description, start, end, href.
|
||||
|
||||
Note: Recurring events (RRULE) are not expanded - only instances that fall
|
||||
within the time range are returned if the server supports it.
|
||||
"""
|
||||
calendar_url = _ensure_trailing_slash(calendar_url)
|
||||
|
||||
# Validate time range
|
||||
if time_max_utc <= time_min_utc:
|
||||
raise ValueError("time_max_utc must be after time_min_utc")
|
||||
|
||||
start_utc = _datetime_to_caldav_utc(time_min_utc)
|
||||
end_utc = _datetime_to_caldav_utc(time_max_utc)
|
||||
@@ -246,7 +279,11 @@ class CalDAVClient:
|
||||
ics = caldata_el.text
|
||||
try:
|
||||
cal = Calendar.from_ical(ics)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
# Log parsing errors but continue with other events
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.debug(f"Failed to parse iCalendar data for event {href}: {e}")
|
||||
continue
|
||||
|
||||
for comp in cal.walk():
|
||||
@@ -258,14 +295,32 @@ class CalDAVClient:
|
||||
continue
|
||||
|
||||
dtstart = comp.get("DTSTART")
|
||||
dtend = comp.get("DTEND")
|
||||
if not dtstart or not dtend:
|
||||
if not dtstart:
|
||||
continue
|
||||
|
||||
start = dtstart.dt
|
||||
end = dtend.dt
|
||||
# Skip all-day events (date)
|
||||
if not isinstance(start, datetime) or not isinstance(end, datetime):
|
||||
# Skip all-day events (date objects, not datetime)
|
||||
if not isinstance(start, datetime):
|
||||
continue
|
||||
|
||||
# Handle DTEND or DURATION
|
||||
dtend = comp.get("DTEND")
|
||||
duration = comp.get("DURATION")
|
||||
|
||||
if dtend:
|
||||
end = dtend.dt
|
||||
if not isinstance(end, datetime):
|
||||
continue
|
||||
elif duration:
|
||||
# Calculate end from start + duration
|
||||
dur = duration.dt
|
||||
if isinstance(dur, timedelta):
|
||||
end = start + dur
|
||||
else:
|
||||
# Skip if duration is not a timedelta
|
||||
continue
|
||||
else:
|
||||
# No DTEND or DURATION - skip this event
|
||||
continue
|
||||
|
||||
summary = str(comp.get("SUMMARY", "")).strip()
|
||||
@@ -349,24 +404,46 @@ class CalDAVCalendarConnector(BaseConnector):
|
||||
Test connectivity and (optionally) discover calendars.
|
||||
"""
|
||||
try:
|
||||
# Check if credentials exist
|
||||
if not self.credentials:
|
||||
return {"success": False, "message": "No credentials configured. Please set up username and password."}
|
||||
|
||||
# Check if we have username and password
|
||||
try:
|
||||
username, password = self._get_basic_creds()
|
||||
except ValueError as e:
|
||||
return {"success": False, "message": f"Missing credentials: {str(e)}. Please configure username and password."}
|
||||
|
||||
cfg = self.integration.config or {}
|
||||
server_url = cfg.get("server_url")
|
||||
calendar_url = cfg.get("calendar_url")
|
||||
|
||||
# Need at least one URL
|
||||
if not server_url and not calendar_url:
|
||||
return {"success": False, "message": "Either server URL or calendar URL must be configured."}
|
||||
|
||||
client = self._client()
|
||||
|
||||
calendars: List[CalDAVCalendar] = []
|
||||
if server_url:
|
||||
calendars = client.discover_calendars(server_url)
|
||||
try:
|
||||
calendars = client.discover_calendars(server_url)
|
||||
except Exception as e:
|
||||
# If discovery fails but we have calendar_url, continue with calendar_url test
|
||||
if not calendar_url:
|
||||
return {"success": False, "message": f"Failed to discover calendars from server: {str(e)}"}
|
||||
|
||||
# If a calendar URL is provided, validate we can run a REPORT against it (lightweight window)
|
||||
if calendar_url:
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
_ = client.fetch_events(calendar_url, now_utc - timedelta(days=1), now_utc + timedelta(days=1))
|
||||
try:
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
_ = client.fetch_events(calendar_url, now_utc - timedelta(days=1), now_utc + timedelta(days=1))
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"Failed to access calendar at {calendar_url}: {str(e)}"}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Connected to CalDAV. Found {len(calendars)} calendars." if server_url else "Connected to CalDAV.",
|
||||
"message": f"Connected to CalDAV. Found {len(calendars)} calendars." if server_url else "Connected to CalDAV calendar.",
|
||||
"calendars": [{"url": c.href, "name": c.name} for c in calendars],
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -379,11 +456,23 @@ class CalDAVCalendarConnector(BaseConnector):
|
||||
"""
|
||||
from app import db
|
||||
from app.models import TimeEntry, Project, IntegrationExternalEventLink
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
if not self.integration or not self.integration.user_id:
|
||||
return {"success": False, "message": "CalDAV integration must be a per-user integration."}
|
||||
|
||||
# Check credentials
|
||||
if not self.credentials:
|
||||
return {"success": False, "message": "No credentials configured. Please set up username and password."}
|
||||
|
||||
try:
|
||||
username, password = self._get_basic_creds()
|
||||
except ValueError as e:
|
||||
return {"success": False, "message": f"Missing credentials: {str(e)}. Please configure username and password."}
|
||||
|
||||
cfg = self.integration.config or {}
|
||||
calendar_url = cfg.get("calendar_url")
|
||||
server_url = cfg.get("server_url")
|
||||
@@ -393,9 +482,11 @@ class CalDAVCalendarConnector(BaseConnector):
|
||||
# Try to discover and use first calendar
|
||||
try:
|
||||
client = self._client()
|
||||
logger.info(f"Discovering calendars from server: {server_url}")
|
||||
calendars = client.discover_calendars(server_url)
|
||||
if calendars:
|
||||
calendar_url = calendars[0].href
|
||||
logger.info(f"Discovered calendar: {calendar_url} ({calendars[0].name})")
|
||||
# Update config with discovered calendar
|
||||
if not self.integration.config:
|
||||
self.integration.config = {}
|
||||
@@ -407,6 +498,7 @@ class CalDAVCalendarConnector(BaseConnector):
|
||||
else:
|
||||
return {"success": False, "message": "No calendars found on server. Please configure calendar URL manually."}
|
||||
except Exception as e:
|
||||
logger.error(f"Could not discover calendars: {e}", exc_info=True)
|
||||
return {"success": False, "message": f"Could not discover calendars: {str(e)}. Please configure calendar URL manually."}
|
||||
else:
|
||||
return {"success": False, "message": "No calendar selected. Please configure calendar URL or server URL first."}
|
||||
@@ -427,8 +519,16 @@ class CalDAVCalendarConnector(BaseConnector):
|
||||
time_min_utc = datetime.now(timezone.utc) - timedelta(days=lookback_days)
|
||||
time_max_utc = datetime.now(timezone.utc) + timedelta(days=7)
|
||||
|
||||
logger.info(f"Fetching events from {calendar_url} between {time_min_utc} and {time_max_utc}")
|
||||
client = self._client()
|
||||
events = client.fetch_events(calendar_url, time_min_utc, time_max_utc)
|
||||
try:
|
||||
events = client.fetch_events(calendar_url, time_min_utc, time_max_utc)
|
||||
logger.info(f"Fetched {len(events)} events from CalDAV calendar")
|
||||
if len(events) == 0:
|
||||
logger.warning(f"No events found in calendar {calendar_url} for time range {time_min_utc} to {time_max_utc}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch events from calendar: {e}", exc_info=True)
|
||||
return {"success": False, "message": f"Failed to fetch events from calendar: {str(e)}"}
|
||||
|
||||
# Preload projects for title matching
|
||||
projects = Project.query.filter_by(status="active").order_by(Project.name).all()
|
||||
@@ -437,9 +537,25 @@ class CalDAVCalendarConnector(BaseConnector):
|
||||
skipped = 0
|
||||
errors: List[str] = []
|
||||
|
||||
if len(events) == 0:
|
||||
# Update integration status even if no events found (this is a successful sync)
|
||||
self.integration.last_sync_at = datetime.utcnow()
|
||||
self.integration.last_sync_status = "success"
|
||||
self.integration.last_error = None
|
||||
db.session.commit()
|
||||
return {
|
||||
"success": True,
|
||||
"imported": 0,
|
||||
"skipped": 0,
|
||||
"synced_items": 0,
|
||||
"errors": [],
|
||||
"message": f"No events found in calendar for the specified time range ({time_min_utc.date()} to {time_max_utc.date()}).",
|
||||
}
|
||||
|
||||
for ev in events:
|
||||
try:
|
||||
uid = ev["uid"]
|
||||
# Check if this event was already imported (idempotency)
|
||||
existing_link = IntegrationExternalEventLink.query.filter_by(
|
||||
integration_id=self.integration.id, external_uid=uid
|
||||
).first()
|
||||
@@ -495,10 +611,28 @@ class CalDAVCalendarConnector(BaseConnector):
|
||||
external_href=ev.get("href"),
|
||||
)
|
||||
db.session.add(link)
|
||||
# Flush to check for duplicate UID constraint violation
|
||||
db.session.flush()
|
||||
|
||||
imported += 1
|
||||
except Exception as e:
|
||||
errors.append(str(e))
|
||||
# Check if it's a duplicate UID error (unique constraint violation)
|
||||
# This can happen in rare race conditions
|
||||
error_str = str(e).lower()
|
||||
if "unique" in error_str or "duplicate" in error_str or "uq_integration_external_uid" in error_str:
|
||||
# Duplicate UID - mark as skipped (likely imported by another process)
|
||||
skipped += 1
|
||||
logger.debug(f"Event {ev.get('uid', 'unknown')} already imported (duplicate UID - race condition)")
|
||||
# Don't rollback - the time_entry might have been created
|
||||
# Just continue to next event
|
||||
else:
|
||||
# Other error - log it and continue
|
||||
error_msg = f"Event {ev.get('uid', 'unknown')}: {str(e)}"
|
||||
errors.append(error_msg)
|
||||
logger.warning(f"Failed to import event {ev.get('uid', 'unknown')}: {e}")
|
||||
# For other errors, we might want to rollback this specific event
|
||||
# but that's complex with SQLAlchemy, so we'll let the final commit handle it
|
||||
# The duplicate check at the start should catch most issues
|
||||
|
||||
# Update integration status
|
||||
self.integration.last_sync_at = datetime.utcnow()
|
||||
@@ -507,12 +641,23 @@ class CalDAVCalendarConnector(BaseConnector):
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Build detailed message
|
||||
if imported == 0 and skipped > 0:
|
||||
message = f"No new events imported ({skipped} already imported, {len(events)} total found)."
|
||||
elif imported == 0:
|
||||
message = f"No events found in calendar for the specified time range ({time_min_utc.date()} to {time_max_utc.date()})."
|
||||
else:
|
||||
message = f"Imported {imported} events ({skipped} skipped, {len(events)} total found)."
|
||||
|
||||
logger.info(f"CalDAV sync completed: {message}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"imported": imported,
|
||||
"skipped": skipped,
|
||||
"synced_items": imported, # For compatibility with scheduled_tasks
|
||||
"errors": errors,
|
||||
"message": f"Imported {imported} events ({skipped} skipped).",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
return {"success": False, "message": "Sync direction not implemented for CalDAV yet."}
|
||||
|
||||
+62
-14
@@ -214,7 +214,15 @@ def view_integration(integration_id):
|
||||
flash(_("Integration not found."), "error")
|
||||
return redirect(url_for("integrations.list_integrations"))
|
||||
|
||||
connector = service.get_connector(integration)
|
||||
# Try to get connector, but handle errors gracefully
|
||||
connector = None
|
||||
connector_error = None
|
||||
try:
|
||||
connector = service.get_connector(integration)
|
||||
except Exception as e:
|
||||
logger.error(f"Error initializing connector for integration {integration_id}: {e}", exc_info=True)
|
||||
connector_error = str(e)
|
||||
|
||||
credentials = IntegrationCredential.query.filter_by(integration_id=integration_id).first()
|
||||
|
||||
# Get recent sync events
|
||||
@@ -231,6 +239,7 @@ def view_integration(integration_id):
|
||||
"integrations/view.html",
|
||||
integration=integration,
|
||||
connector=connector,
|
||||
connector_error=connector_error,
|
||||
credentials=credentials,
|
||||
recent_events=recent_events,
|
||||
)
|
||||
@@ -325,10 +334,12 @@ def caldav_setup():
|
||||
return redirect(url_for("integrations.list_integrations"))
|
||||
integration = result["integration"]
|
||||
|
||||
connector = service.get_connector(integration)
|
||||
if not connector:
|
||||
flash(_("Could not initialize CalDAV connector."), "error")
|
||||
return redirect(url_for("integrations.list_integrations"))
|
||||
# Try to get connector, but don't fail if credentials are missing (user is setting up)
|
||||
connector = None
|
||||
try:
|
||||
connector = service.get_connector(integration)
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not initialize CalDAV connector (may be normal during setup): {e}")
|
||||
|
||||
# Get user's active projects for default project selection
|
||||
projects = Project.query.filter_by(status="active").order_by(Project.name).all()
|
||||
@@ -341,6 +352,7 @@ def caldav_setup():
|
||||
calendar_name = request.form.get("calendar_name", "").strip()
|
||||
default_project_id = request.form.get("default_project_id", "").strip()
|
||||
verify_ssl = request.form.get("verify_ssl") == "on"
|
||||
auto_sync = request.form.get("auto_sync") == "on"
|
||||
lookback_days_str = request.form.get("lookback_days", "90") or "90"
|
||||
|
||||
# Validation
|
||||
@@ -349,6 +361,25 @@ def caldav_setup():
|
||||
if not server_url and not calendar_url:
|
||||
errors.append(_("Either server URL or calendar URL is required."))
|
||||
|
||||
# Validate URL format if provided
|
||||
if server_url:
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(server_url)
|
||||
if not parsed.scheme or not parsed.netloc:
|
||||
errors.append(_("Server URL must be a valid URL (e.g., https://mail.example.com/dav)."))
|
||||
except Exception:
|
||||
errors.append(_("Server URL format is invalid."))
|
||||
|
||||
if calendar_url:
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(calendar_url)
|
||||
if not parsed.scheme or not parsed.netloc:
|
||||
errors.append(_("Calendar URL must be a valid URL."))
|
||||
except Exception:
|
||||
errors.append(_("Calendar URL format is invalid."))
|
||||
|
||||
# Check if we need to update credentials (username provided or password provided)
|
||||
existing_creds = IntegrationCredential.query.filter_by(integration_id=integration.id).first()
|
||||
needs_creds_update = username or password or not existing_creds
|
||||
@@ -403,18 +434,35 @@ def caldav_setup():
|
||||
integration.config["sync_direction"] = "calendar_to_time_tracker" # MVP: import only
|
||||
integration.config["lookback_days"] = lookback_days
|
||||
integration.config["default_project_id"] = int(default_project_id)
|
||||
integration.config["auto_sync"] = auto_sync
|
||||
|
||||
# Save credentials (only if username/password provided)
|
||||
if username and (password or existing_creds):
|
||||
service.save_credentials(
|
||||
integration_id=integration.id,
|
||||
access_token=password if password else (existing_creds.access_token if existing_creds else ""),
|
||||
refresh_token=None,
|
||||
expires_at=None,
|
||||
token_type="Basic",
|
||||
scope="caldav",
|
||||
extra_data={"username": username},
|
||||
)
|
||||
# Use existing password if new password not provided
|
||||
password_to_save = password if password else (existing_creds.access_token if existing_creds else "")
|
||||
if password_to_save:
|
||||
result = service.save_credentials(
|
||||
integration_id=integration.id,
|
||||
access_token=password_to_save,
|
||||
refresh_token=None,
|
||||
expires_at=None,
|
||||
token_type="Basic",
|
||||
scope="caldav",
|
||||
extra_data={"username": username},
|
||||
)
|
||||
if not result.get("success"):
|
||||
flash(_("Failed to save credentials: %(message)s", message=result.get("message", "Unknown error")), "error")
|
||||
return render_template(
|
||||
"integrations/caldav_setup.html",
|
||||
integration=integration,
|
||||
connector=connector,
|
||||
projects=projects,
|
||||
)
|
||||
|
||||
# Ensure integration is active if credentials exist
|
||||
credentials_check = IntegrationCredential.query.filter_by(integration_id=integration.id).first()
|
||||
if credentials_check:
|
||||
integration.is_active = True
|
||||
|
||||
if safe_commit("caldav_setup", {"integration_id": integration.id}):
|
||||
flash(_("CalDAV integration configured successfully."), "success")
|
||||
|
||||
@@ -174,6 +174,20 @@
|
||||
{{ _('How many days back to import events from (default: 90).') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="auto_sync"
|
||||
id="auto_sync"
|
||||
{% if integration.config and integration.config.get('auto_sync', True) %}checked{% endif %}
|
||||
class="mr-2">
|
||||
<span class="text-sm">{{ _('Enable automatic sync') }}</span>
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark ml-6">
|
||||
{{ _('Automatically sync calendar events every hour. You can still trigger manual syncs.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -106,25 +106,45 @@
|
||||
<div>
|
||||
<div class="bg-card-light dark:bg-card-dark p-6 rounded-lg shadow">
|
||||
<h3 class="text-lg font-semibold mb-4">{{ _('Actions') }}</h3>
|
||||
{% if connector_error %}
|
||||
<div class="mb-4 p-3 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded-lg">
|
||||
<p class="text-sm text-red-800 dark:text-red-200">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||
{{ _('Connector Error') }}: {{ connector_error }}
|
||||
</p>
|
||||
{% if integration.provider == 'caldav_calendar' %}
|
||||
<a href="{{ url_for('integrations.caldav_setup') }}" class="mt-2 inline-block text-sm text-red-800 dark:text-red-200 hover:underline">
|
||||
{{ _('Configure CalDAV Integration') }} →
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="space-y-2">
|
||||
{% if connector %}
|
||||
<form method="POST" action="{{ url_for('integrations.test_integration', integration_id=integration.id) }}" class="mb-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="w-full bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
|
||||
<button type="submit" class="w-full bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors {% if not connector %}opacity-50 cursor-not-allowed{% endif %}" {% if not connector %}disabled{% endif %}>
|
||||
<i class="fas fa-vial mr-2"></i>{{ _('Test Connection') }}
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('integrations.sync_integration', integration_id=integration.id) }}" class="mb-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="w-full bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors">
|
||||
<button type="submit" class="w-full bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors {% if not connector %}opacity-50 cursor-not-allowed{% endif %}" {% if not connector %}disabled{% endif %}>
|
||||
<i class="fas fa-sync mr-2"></i>{{ _('Sync Now') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('integrations.delete_integration', integration_id=integration.id) }}" onsubmit="return confirm('{{ _('Are you sure you want to delete this integration?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="w-full bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors">
|
||||
<i class="fas fa-trash mr-2"></i>{{ _('Delete') }}
|
||||
</button>
|
||||
</form>
|
||||
{% if integration.provider == 'caldav_calendar' %}
|
||||
<a href="{{ url_for('integrations.caldav_setup') }}" class="block w-full bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors text-center">
|
||||
<i class="fas fa-cog mr-2"></i>{{ _('Edit Configuration') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user