Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions app/config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ tracy:
name: "UTE"
host: "timemachine1sql.berea.edu"
user: "ute_limited"
password: "OVERRIDE"
password: "Edit local-override.yml"

ldap:
user: "BPLP"
password: "OVERRIDE"
password: "Edit local-override.yml"

files:
base_path: 'app/static/files'
Expand All @@ -32,6 +32,13 @@ files:
course_attachment_path: 'courseattachments'
proposal_attachment_path: 'proposalattachments'

campusgroups:
url: https://berea-sandbox.campusgroups.com
api_path: WebServices/campusgroups.asmx
secret: 'Edit local-override.yml'
key: 'Edit local-override.yml'
school: 'berea-sandbox'

MAIL_ENABLED: True
MAIL_SERVER: "smtp.gmail.com" # use smtp.gmail.com for gmail, mail.berea.edu for Berea
MAIL_PORT: 465 # use port 465 for gmail, use 25 for Berea
Expand Down
6 changes: 5 additions & 1 deletion app/config/production.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use_shibboleth: True
show_queries: False

campusgroups:
url: https://berea.campusgroups.com
school: 'berea'

MAIL_ENABLED: True
MAIL_SERVER: "mail.berea.edu"
MAIL_PORT: 587
MAIL_USERNAME: 'ssdt@berea.edu'
MAIL_PASSWORD: ''
MAIL_PASSWORD: 'Edit local-override.yml'
MAIL_USE_TLS: True
MAIL_USE_SSL: False
MAIL_DEFAULT_SENDER: 'support@bereacollege.onmicrosoft.com'
Expand Down
7 changes: 7 additions & 0 deletions app/controllers/admin/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ def eventDisplay(eventId):

# Event Edit
if 'edit' in rule.rule:
event.isCampusGroupsSynced = False
event.save(only=[Event.isCampusGroupsSynced])
return render_template("events/createEvent.html",
eventData = eventData,
termList = Term.select().order_by(Term.termOrder),
Expand Down Expand Up @@ -375,6 +377,11 @@ def eventDisplay(eventId):

userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term)

# CampusGroups integration
if event.campusGroupsId:
eventData["campusGroupsURL"] = event.campusGroupsURL
eventData["campusGroupsId"] = event.campusGroupsId
eventData["isCampusGroupsSynced"] = event.isCampusGroupsSynced
return render_template("events/eventView.html",
eventData=eventData,
event=event,
Expand Down
31 changes: 30 additions & 1 deletion app/controllers/events/routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from flask import Flask, redirect, flash, url_for, request, render_template, g, json, abort, session
from app.logic.campusGroups import CampusGroups
from flask import Flask, redirect, flash, url_for, request, render_template, g, json, abort, session, jsonify
import requests, xmltodict
from datetime import datetime
from peewee import DoesNotExist

Expand All @@ -12,6 +14,9 @@
from app.logic.emailHandler import EmailHandler
from app.logic.participants import addBnumberAsParticipant

from app import app


@events_bp.route('/event/<eventid>/scannerentry', methods=['GET'])
def loadKiosk(eventid):
"""Renders kiosk for specified event."""
Expand Down Expand Up @@ -48,3 +53,27 @@ def kioskSignin():
except Exception as e:
print("Error in Kiosk Page", e)
return "", 500

@events_bp.route('/retrieveEvents', methods=['GET'])
def retrieveEvents():
try:
cg = CampusGroups()
events = cg.getEvents()
return events

except requests.exceptions.RequestException as e:
print("Error retrieving data from campusgroups: \n", e)
return "", 500


@events_bp.route('/addEventToCampusGroups/<event_id>', methods=['GET'])
def addEventToCampusGroups(event_id):
try:
cg = CampusGroups()
data = cg.parseEventData(event_id)
response = cg.addEvent(data)
return response # FIXME Return a better response, or parse this in the front end

except requests.exceptions.RequestException as e:
print("Error retrieving data from campusgroups: \n", e)
return "", 500
186 changes: 186 additions & 0 deletions app/logic/campusGroups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
from datetime import datetime
from os import abort
from flask import abort, jsonify
import requests, xmltodict
import xml.etree.ElementTree as ET

from app import app
from app.models.event import Event

EVENT_FIELDS = [
"cg_event_id", # Integer — 0 to create, existing id to update
"cg_group_acronym", # String
"external_event_id", # String[100]
"event_coordinator", # String[300], email
"event_name", # String
"quick_description", # String[4294967295]
"event_type", # String[255]
"event_start_date", # String[16]
"event_start_time", # String[16]
"event_end_date", # String[16]
"event_end_time", # String[16]
"event_location", # String[255]
"room_id", # Integer
"event_display_to", # Integer - see docs for magic numbers
"allow_rsvp", # Integer, 0 or 1
"event_open_to", # Integer - see docs for magic numbers
"delete_event", # Integer, 0 or 1
"hide_from_events_slider", # Integer, 0 or 1
"force_display_on_rooms_schedule", # Integer, 0 or 1
"location_type", # Integer, 0/2/3
"event_audience", # Integer
]

REQUIRED_FIELDS = [
"cg_group_acronym",
"event_name",
"event_start_date",
"event_start_time",
"event_end_date",
"event_end_time"
]

# The following are also required fields, but are NOT passed into CampusGroups objects (they are retrieved from the config files):
# "api_key", # String[100], Required
# "timestamp", # String[20], yyyy-MM-ddTHH:mm:ssZ (UTC)
# "school", # String[30]

class CampusGroups:
def __init__(self):
self.url = app.config["campusgroups"]["url"]
self.api_path = app.config["campusgroups"]["api_path"]
self.key = app.config["campusgroups"]["key"]
self.school = app.config["campusgroups"]["school"]
self.headers = {'X-CG-API-Secret': app.config["campusgroups"]["secret"]}
self.event = None

def getEvents(self):
"""
Retrieve events from CampusGroups using the RSS feed.
"""
now = datetime.now()
ts_now = now.isoformat().replace('+00:00', 'Z')
rss_events_url = f'{self.url}/rss_events?ts={ts_now}&preauth={self.key}'
response = requests.get(rss_events_url, headers = self.headers)
response.raise_for_status()
data_dict = xmltodict.parse(response.text)
return jsonify(data_dict)


def addEvent(self, eventData):
"""
Add or update an event in CampusGroups using the CreateUpdateEvent SOAP API.
"""
createUpdateEvent_url = f'{self.url}/{self.api_path}?op=CreateUpdateEvent'
xmlOut = self.build_event_xml(eventData)

self.headers["SOAPAction"] = "http://campusgroups.com/CreateUpdateEvent"
self.headers["Content-Type"] = "text/xml; charset=utf-8"

response = requests.post(createUpdateEvent_url, data=xmlOut, headers=self.headers, timeout=15)
response.raise_for_status()

return self.parse_event_response(response.content)


def build_event_xml(self, payload):
"""
Build the SOAP XML body for CreateUpdateEvent from a dictionary payload.
"""
missing = [f for f in REQUIRED_FIELDS if not payload.get(f)]
if missing:
raise ValueError(f"Missing required field(s): {', '.join(missing)}")

envelope = ET.Element(
"soap12:Envelope",
{
"xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
"xmlns:soap12": "http://www.w3.org/2003/05/soap-envelope",
},
)
body = ET.SubElement(envelope, "soap12:Body")
op = ET.SubElement(body, "CreateUpdateEvent", {"xmlns": "http://campusgroups.com/"})

# Always-required auth/context fields
now = datetime.now()
ts_now = now.isoformat().replace('+00:00', 'Z')

ET.SubElement(op, "timestamp").text = (ts_now)
ET.SubElement(op, "school").text = self.school
ET.SubElement(op, "api_key").text = self.key
# Event-specific fields — only send what's provided so partial
# updates don't clobber existing values.
for field in EVENT_FIELDS:
value = payload.get(field)
if value is not None and value != "":
ET.SubElement(op, field).text = str(value)

xml_bytes = ET.tostring(envelope, encoding="utf-8")
return b'<?xml version="1.0" encoding="utf-8"?>' + xml_bytes

def parseEventData(self, eventID):
"""
Parse the event data from Celts-link database into a dictionary.
"""

# TODO handle recurring events
try:
self.event = Event.get_by_id(eventID)
except Event.DoesNotExist:
abort(404, description=f"No events with ID {eventID} found.")
data = {}

if self.event.campusGroupsId is None:
data["cg_event_id"] = 0 # create a new event
else:
data["cg_event_id"] = self.event.campusGroupsId #update an existing event

data["cg_group_acronym"] = "Celts" # event.program?
data["external_event_id"] = self.event.id
data["event_coordinator"] = self.event.contactEmail
data["event_name"] = self.event.name
data["quick_description"] = self.event.description
data["event_type"] = "Academic"
data["event_start_date"] = self.event.startDate.strftime("%Y-%m-%d")
data["event_start_time"] = self.event.timeStart.strftime("%H:%M")
data["event_end_date"] = self.event.startDate.strftime("%Y-%m-%d")
data["event_end_time"] = self.event.timeEnd.strftime("%H:%M")
data["event_location"] = self.event.location

# Check CampusGroups API documentation for magic numbers:
data["event_display_to"] = 0
data["allow_rsvp"] = 1
data["event_open_to"] = 1
data["delete_event"] = 0
data["hide_from_events_slider"] = 0
data["force_display_on_rooms_schedule"] = 0
data["location_type"] = 0

return data

def parse_event_response(self, xml_content):
"""
Pull cg_event_id / message / message_code out of the SOAP response,
tolerating either the namespaced or bare tag form.
"""

responseData = xmltodict.parse(xml_content)

def find_text(tag):
el = responseData['soap:Envelope']['soap:Body']['CreateUpdateEventResponse']['CreateUpdateEventResult'].get(tag)
return el if el is not None else None

response = {
"cg_event_id": find_text("cg_event_id"),
"message": find_text("message"),
"message_code": find_text("message_code"),
}

if response["cg_event_id"]:
self.event.campusGroupsId = response["cg_event_id"]
self.event.campusGroupsURL = f"{self.url}/celts/rsvp_boot?id={response['cg_event_id']}"
self.event.isCampusGroupsSynced = True
self.event.save()

return response
3 changes: 3 additions & 0 deletions app/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class Event(baseModel):
isCanceled = BooleanField(default=False)
deletionDate = DateTimeField(null=True)
deletedBy = TextField(null=True)
campusGroupsId = IntegerField(null=True)
campusGroupsURL = CharField(null=True)
isCampusGroupsSynced = BooleanField(default=False)
eventFlagsMatrix = {'isAllVolunteerTraining': {'isAllVolunteerTraining',
'isTraining'},
'isCeltsTraining': {'isCeltsTraining',
Expand Down
19 changes: 19 additions & 0 deletions app/static/js/eventList.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,27 @@ $(document).ready(function(){
$(".no-upcoming").show()
}
}


});

$("#pushToCampusGroupsBtn").on("click", function(){
$.ajax({
url: "/addEventToCampusGroups/" + $("#pushToCampusGroupsBtn").val(),
type: "GET",
success: function(response) {
console.log(response)
alert("Event pushed to CampusGroups successfully!");
location.reload();
},
error: function(xhr, status, error) {
var response = JSON.parse(xhr.responseText);
alert("Error pushing event to CampusGroups: " + response);
}
})
});


function rsvpForEvent(eventID){
rsvpInfo = {id: eventID,
from: 'ajax'}
Expand Down
2 changes: 1 addition & 1 deletion app/templates/events/eventNav.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
<div class="btn-group">
<ul class="nav nav-tabs nav-fill mx-3 mb-3" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link {{ 'active' if tabName == 'view' }}" href="/event/{{event.id}}/view" role="tab" aria-selected="{{ 'true' if tabName == 'view' else 'false'}}">View Event</a>
<a class="nav-link {{ 'active' if tabName == 'view' }}" href="/event/{{event.id}}/view" role="tab" aria-selected="{{ 'true' if tabName == 'view' else 'false'}}">View Event {% if not event.isCampusGroupsSynced %}<i class="bi bi-arrow-up-circle" style="color: red;"> </i>{% endif %}</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link {{ 'active' if tabName == 'edit' }}" href="/event/{{event.id}}/edit" role="tab" aria-selected="{{ 'true' if tabName == 'edit' else 'false'}}">Edit Event</a>
Expand Down
17 changes: 16 additions & 1 deletion app/templates/events/eventView.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,23 @@ <h5 id="time" style="margin: 0;">Time:</h5>
<div>
<h5 id="location"style="margin: 0;">Location:</h5>
<p style="margin: 0;">{{eventData.location}}</p>
</div>
</div>
</div>
<br>
<div class="d-flex align-top">
<i class="bi bi-arrow-bar-right" style="margin-right: 10px;"></i>
<div>
<h5 id="location" style="margin: 0;">CampusGroups:</h5>
{% if eventData.campusGroupsId %}
<p style="margin: 0;"><a href="{{eventData.campusGroupsURL|safe}}" target="_blank" >CampusGroups Event</a></p>
{% if not eventData.isCampusGroupsSynced %}
<i class="bi bi-arrow-up-circle" style="color: red;"> </i>Update event changes to CampusGroups? <button type="button" class="btn btn-primary" value="{{eventData.id}}" id="pushToCampusGroupsBtn">Push</button>
{% endif %}
{% else %}
Add this event to CampusGroups? <button type="button" class="btn btn-primary" value="{{eventData.id}}" id="pushToCampusGroupsBtn">Push</button>
{% endif %}
</div>
</div>
<br>
<div>
{% if eventData.isRsvpRequired %}
Expand Down
Loading
Loading