Coverage for app/logic/campusGroups.py: 63%
91 statements
« prev ^ index » next coverage.py v7.10.2, created at 2026-07-08 20:07 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2026-07-08 20:07 +0000
1from datetime import datetime
2from os import abort
3from flask import abort, jsonify
4import requests, xmltodict
5import xml.etree.ElementTree as ET
7from app import app
8from app.models.event import Event
10EVENT_FIELDS = [
11 "cg_event_id", # Integer — 0 to create, existing id to update
12 "cg_group_acronym", # String
13 "external_event_id", # String[100]
14 "event_coordinator", # String[300], email
15 "event_name", # String
16 "quick_description", # String[4294967295]
17 "event_type", # String[255]
18 "event_start_date", # String[16]
19 "event_start_time", # String[16]
20 "event_end_date", # String[16]
21 "event_end_time", # String[16]
22 "event_location", # String[255]
23 "room_id", # Integer
24 "event_display_to", # Integer - see docs for magic numbers
25 "allow_rsvp", # Integer, 0 or 1
26 "event_open_to", # Integer - see docs for magic numbers
27 "delete_event", # Integer, 0 or 1
28 "hide_from_events_slider", # Integer, 0 or 1
29 "force_display_on_rooms_schedule", # Integer, 0 or 1
30 "location_type", # Integer, 0/2/3
31 "event_audience", # Integer
32]
34REQUIRED_FIELDS = [
35 "cg_group_acronym",
36 "event_name",
37 "event_start_date",
38 "event_start_time",
39 "event_end_date",
40 "event_end_time"
41]
43# The following are also required fields, but are NOT passed into CampusGroups objects (they are retrieved from the config files):
44 # "api_key", # String[100], Required
45 # "timestamp", # String[20], yyyy-MM-ddTHH:mm:ssZ (UTC)
46 # "school", # String[30]
48class CampusGroups:
49 def __init__(self, campusGroupsEnv = "sandbox"):
50 self.url = app.config["campusgroups"][campusGroupsEnv]["url"]
51 self.secret = app.config["campusgroups"][campusGroupsEnv]["secret"]
52 self.key = app.config["campusgroups"][campusGroupsEnv]["key"]
53 self.school = app.config["campusgroups"][campusGroupsEnv]["school"]
54 self.headers = {'X-CG-API-Secret': self.secret}
55 self.event = None
57 def getEvents(self):
58 """
59 Retrieve events from CampusGroups using the RSS feed.
60 """
61 now = datetime.now()
62 ts_now = now.isoformat().replace('+00:00', 'Z')
63 rss_events_url = f'{self.url}/rss_events?ts={ts_now}&preauth={self.key}'
64 response = requests.get(rss_events_url, headers = self.headers)
65 response.raise_for_status()
66 data_dict = xmltodict.parse(response.text)
67 return jsonify(data_dict)
70 def addEvent(self, eventData):
71 """
72 Add or update an event in CampusGroups using the CreateUpdateEvent SOAP API.
73 """
74 createUpdateEvent_url = "https://berea-sandbox.campusgroups.com/WebServices/campusgroups.asmx?op=CreateUpdateEvent"
75 xmlOut = self.build_event_xml(eventData)
77 self.headers["SOAPAction"] = "http://campusgroups.com/CreateUpdateEvent"
78 self.headers["Content-Type"] = "text/xml; charset=utf-8"
81 response = requests.post(createUpdateEvent_url, data=xmlOut, headers=self.headers, timeout=15)
82 response.raise_for_status()
84 return self.parse_event_response(response.content)
87 def build_event_xml(self, payload):
88 """
89 Build the SOAP XML body for CreateUpdateEvent from a dictionary payload.
90 """
91 missing = [f for f in REQUIRED_FIELDS if not payload.get(f)]
92 if missing:
93 raise ValueError(f"Missing required field(s): {', '.join(missing)}")
95 envelope = ET.Element(
96 "soap12:Envelope",
97 {
98 "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
99 "xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
100 "xmlns:soap12": "http://www.w3.org/2003/05/soap-envelope",
101 },
102 )
103 body = ET.SubElement(envelope, "soap12:Body")
104 op = ET.SubElement(body, "CreateUpdateEvent", {"xmlns": "http://campusgroups.com/"})
106 # Always-required auth/context fields
107 now = datetime.now()
108 ts_now = now.isoformat().replace('+00:00', 'Z')
110 ET.SubElement(op, "timestamp").text = (ts_now)
111 ET.SubElement(op, "school").text = self.school
112 ET.SubElement(op, "api_key").text = self.key
113 # Event-specific fields — only send what's provided so partial
114 # updates don't clobber existing values.
115 for field in EVENT_FIELDS:
116 value = payload.get(field)
117 if value is not None and value != "":
118 ET.SubElement(op, field).text = str(value)
120 xml_bytes = ET.tostring(envelope, encoding="utf-8")
121 return b'<?xml version="1.0" encoding="utf-8"?>' + xml_bytes
123 def parseEventData(self, eventID):
124 """
125 Parse the event data from Celts-link database into a dictionary.
126 """
128 # TODO handle recurring events
129 try:
130 self.event = Event.get_by_id(eventID)
131 except Event.DoesNotExist:
132 abort(404, description=f"No events with ID {eventID} found.")
133 data = {}
135 if self.event.campusGroupsId is None:
136 data["cg_event_id"] = 0 # create a new event
137 else:
138 data["cg_event_id"] = self.event.campusGroupsId #update an existing event
140 data["cg_group_acronym"] = "Celts" # event.program?
141 data["external_event_id"] = self.event.id
142 data["event_coordinator"] = self.event.contactEmail
143 data["event_name"] = self.event.name
144 data["quick_description"] = self.event.description
145 data["event_type"] = "Academic"
146 data["event_start_date"] = self.event.startDate.strftime("%Y-%m-%d")
147 data["event_start_time"] = self.event.timeStart.strftime("%H:%M")
148 data["event_end_date"] = self.event.startDate.strftime("%Y-%m-%d")
149 data["event_end_time"] = self.event.timeEnd.strftime("%H:%M")
150 data["event_location"] = self.event.location
152 # Check CampusGroups API documentation for magic numbers:
153 data["event_display_to"] = 0
154 data["allow_rsvp"] = 1
155 data["event_open_to"] = 1
156 data["delete_event"] = 0
157 data["hide_from_events_slider"] = 0
158 data["force_display_on_rooms_schedule"] = 0
159 data["location_type"] = 0
161 return data
163 def parse_event_response(self, xml_content):
164 """
165 Pull cg_event_id / message / message_code out of the SOAP response,
166 tolerating either the namespaced or bare tag form.
167 """
169 responseData = xmltodict.parse(xml_content)
171 def find_text(tag):
172 el = responseData['soap:Envelope']['soap:Body']['CreateUpdateEventResponse']['CreateUpdateEventResult'].get(tag)
173 return el if el is not None else None
175 response = {
176 "cg_event_id": find_text("cg_event_id"),
177 "message": find_text("message"),
178 "message_code": find_text("message_code"),
179 }
181 if response["cg_event_id"]:
182 self.event.campusGroupsId = response["cg_event_id"]
183 self.event.campusGroupsURL = f"{self.url}/celts/rsvp_boot?id={response['cg_event_id']}"
184 self.event.isCampusGroupsSynced = True
185 self.event.save()
187 return response