The API documentation needs to show POST data

On the API documentation page (https://troop466.trooptrack.com/api/swagger) it would be very helpful to show any POST data for those APIs using POST. For example, /v1/events uses POST to create the event, but the “try it out” only shows “Request URL”, but not the Request Data – which would be very useful to debug issues.

Thanks!!

2 Likes

I agree. The POST endpoints are terribly difficult to figure out how to make work, which is discouraging.

Yes, and after a ton of hours this week, I think I’ve finally mastered them (using python no less)

OK, so here is some python code for any hackers that can grok it :grin:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import argparse
import inspect
import sys
import os
import requests
import json
import datetime
from subprocess import Popen, PIPE

args = None

###
### begin - this is the start of the program.  begin is actually called at the end of this file.
### 
### parse the command line then create events
###
def begin():
    parser = argparse.ArgumentParser()
    parser.add_argument("-v","--verbose", help="increase verbosity. use more than once to increase verbosity", action="count")
    parser.add_argument("-t","--trooptrack", help="connect to, and then add the calendar to TroopTrack system using the login username/password", metavar='<username/password>')
    
    global args
    args = parser.parse_args()

    if args.verbose : print("We be up!")
    else: args.verbose = 0

    tt = trooptrack()
    events = [{'description': 'Regular Troop Meetings on 1st & 3rd Tuesdays of each month.\nArrive promptly for sign offs with Scoutmaster or Assistant Scoutmaster, meeting starts at 15 after.\n\n<b>Wear class A uniform</b>\n', 'start': datetime.datetime(2014, 1, 2, 19, 0), 'end': datetime.datetime(2014, 1, 2, 20, 30), 'who': ['everyone'], 'where': 'Church', 'invite': '1', 'title': 'Troop Meeting', 'type': 'Meeting'}]
    for x in events:
        tt.newEvent(x)

    if args.verbose : print("### DONE ####")



###
### AttrDisplay - base class that pretty prints its class
### 
class AttrDisplay:
    def gatherAttrs(self):
        attrs = []
        for key in sorted(self.__dict__):
            attrs.append('{}: {}'.format(key, getattr(self,key)))
        return "\n".join(attrs)
    def __repr__(self):
        return '{} = [\n{}\n]\n\n'.format(self.__class__.__name__, self.gatherAttrs())



def doAppleScript(scpt):
    if type(scpt) == type('') :
        scpt = str.encode(scpt)
    rslts = ex(['/usr/bin/osascript', '-'], stdin=scpt)
    if rslts['rc'] != 0 :
        print( '### Command:\n\t{}\n\tfails with:\n\t{}'.format(rslts['command'], rslts['stderr']) )
        sys.exit(rslts['rc'])
    return rslts


def ex(args, stdin=None):
    io = {}
    io['command'] = ' '.join(map(str,args))
    if stdin != None:
        io['command'] = io['command'] + " << " + stdin.decode("utf-8")
        proc = Popen(args, stdout=PIPE, stderr=PIPE, stdin=PIPE, close_fds=True)
        io['stdout'], io['stderr'] = proc.communicate(stdin)
        io['stdout'] = io['stdout'].decode("utf-8").rstrip()
        io['stderr'] = io['stderr'].decode("utf-8").rstrip()
        io['rc'] = proc.returncode
        return io
    
    proc = Popen(args, stdout=PIPE, stderr=PIPE, close_fds=True)
    proc.wait()
    io['stdout'] = proc.stdout.read().decode("utf-8").rstrip()
    io['stderr'] = proc.stderr.read().decode("utf-8").rstrip()
    io['rc'] = proc.returncode
    return io

def weekdayToString(w):
    junk = "Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday".split("|")
    return junk[w]

def lastWeekday(date, weekday):
    daycount = monthrange(date.year, date.month)[1]
    date = date.replace(day=daycount)
    while weekdayToString(date.weekday()) != weekday:
        daycount -= 1
        date = date.replace(day=daycount)
    return date
    
def nthWeekday(date, n, weekday):
    if n < 0: return lastWeekday(date, weekday)
    
    date = date.replace(day=1)

    while weekdayToString(date.weekday()) != weekday:
        #print("WDTS:{},{},{} {},{}".format(date,n,weekday,date.weekday(),weekdayToString(date.weekday())))
        date = date + datetime.timedelta(days=1)

    n = n - 1
    while n:
        date = date + datetime.timedelta(days=7)
        n = n - 1
    
    return date



def printDict(t,x):
    if t != None : print(t,end="")
    pprint.pprint(x)
    return

def __LINE__():
    callerframerecord = inspect.stack()[2]    # 0 represents this line
                                                # 1 represents line at caller
    frame = callerframerecord[0]
    info = inspect.getframeinfo(frame)
    return info.lineno                         # __LINE__     -> 13

def errorExit(s):
    print("\n### ERROR ### (line {})\n{}\n".format(__LINE__(),s))
    sys.exit(1)

def warning(s):
    print("\n### WARNING ### (line {})\n{}\n".format(__LINE__(),s))

def getDSTOffset(d):
    ##  for 2017 => Sun, Mar 12, 2:00 AM	PST → PDT	+1 hour (DST start)	UTC-7h  (2nd sunday)
 	##              Sun, Nov 5, 2:00 AM	PDT → PST	-1 hour (DST end)	UTC-8h    (1st sunday) 	
    startPDT =  datetime.datetime(d.year,3,nthWeekday(datetime.date(d.year,3,1), 2, "Sunday").day,2)    ## 3/12/2017 2:00AM
    endPDT = datetime.datetime(d.year,11,nthWeekday(datetime.date(d.year,11,1), 1, "Sunday").day,1)      ##11/5/2017 2:00AM
    if args.verbose > 3 :
        print("startPDT: {}".format(startPDT))
        print("endPDT: {}".format(endPDT))
        print("###### PDT {} #####".format(d) )

    if ( d > startPDT and d < endPDT ):
        return -1
    return 0

class trooptrack(AttrDisplay):
    # setup program globals
    url = 'https://YOUR_TROOP_HERE.trooptrack.com:443/api/v1/'
    headers = {'X-Partner-Token': 'PARTNER_TOKEN_HERE'}

    def __init__(self):
        if self.headers['X-Partner-Token'] == 'PARTNER_TOKEN_HERE':
            errorExit("You have to edit the code to add your troop number and partner token.")

        parts = args.trooptrack.split("/")
        if len(parts) > 2 :
            errorExit("--trooptrack <username/password> had more than one /")

        if args.verbose :
            print("### Connecting to TroopTrack...")

        r = self.ttRequest('tokens', data={}, headers={'X-Username':parts[0],'X-User-Password':parts[1]})
        rsp = r.json()['users'][0]
        token = rsp['token']
        self.troopID = rsp['troop_id']
        self.userID = rsp['user_id']
        if args.verbose > 1 :
            print ("Token {}".format(token) )
            print ("TroopID {}".format(self.troopID) )
            print ("UserID {}".format(self.userID) )

        self.headers['X-User-Token'] = token
        self.headers['Connection']   = 'keep-alive'
        
        if args.verbose : 
            print("### Connected")
        
        ## get the event types
        r = self.ttRequest('events/types').json()

        # extract users
        self.users = r['users']                  ## 'user_id': 3778, 'name': 'first last', 'scout': True

        # calendar "event" types
        self.event_types = {}
        for item in r['event_types'] :          ## 'color': 'white', 'name': 'Campout', 'event_type_id': 66837
            self.event_types[item['name']] = str(item['event_type_id'])

        # array of calendar event invitees
        self.patrols = {}
        for item in r['patrols']:
            self.patrols[item['name'].lower()] = 'Patrol-{}'.format(item['patrol_id'])
        self.patrols['everyone'] = 'Troop-{}'.format(self.troopID)   ## special 'patrol' for everyone
        if args.verbose > 1:
            printDict("#PATROLS:",self.patrols)

        # array of mailing lists (custom mailing list names)
        self.mailingLists = {}
        r = self.ttRequest('mailing_lists').json()['mailing_lists']
        for l in r:
            self.mailingLists[l['name'].lower()] = {'id':l['mailing_list_id'],'email':l['email']}
        
        if args.verbose > 1: 
            print("### {} event types".format(len(self.event_types)))
            print("### {} patrols".format(len(self.patrols)))
            print("### {} users".format(len(self.users)))


    def ttDate(self,d):
        return "{:4d}-{:02d}-{:02d}".format(d.year,d.month,d.day)

    def ttDatetime(self,d):
        gmt = 8 + getDSTOffset(d)
        #print("GMT offst is {} for {}".format(gmt,d))
        rslt = "{:4d}-{:02d}-{:02d}T{:02d}:{:02d}-0{}00".format(d.year,d.month,d.day,d.hour,d.minute,gmt)
        #print("TT DATE {}".format(rslt))
        return rslt


    def ttRequest(self,req,data=None,headers=None):
        # all the heavy lifting happens in here
        requestURL = self.url + req

        requestHeaders = dict(self.headers)
        if headers != None :
            for k,v in headers.items():
                requestHeaders[k] = v

        if args.verbose > 2 :
            print("#######  #######  #######  #######  #######  #######  #######\nttRequest url: {}".format(requestURL))
            printDict("## headers:", requestHeaders)
            if data != None :
                printDict("## data:", data)
            print("#######\n")
        
        if 'Content-Type' in requestHeaders and requestHeaders['Content-Type'] == "application/json":
            data = json.dumps(data)
            
        if data != None :
            rslt = requests.post(requestURL, headers=requestHeaders, data=data)     # probably need to be surrounded by a "try"
        else:
            rslt = requests.get(requestURL, headers=requestHeaders)                  # probably need to be surrounded by a "try"

        if rslt.status_code < 200 or rslt.status_code > 201 :
            printDict("#request headers:", rslt.request.headers)
            print("#request body: ", rslt.request.body)
            print("#result reason: ", rslt.reason)
            printDict("#result headers: ", rslt.headers)
            print("#result text: ", rslt.text)
            errorExit('TroopTrack request failed with HTTP result code ({}) '.format(rslt.status_code))
        
        if args.verbose > 1 :
            j = rslt.json()
            ## keep the noise down
            if 'event' in j:
                j = j['event']  
            if 'event_types' in j and 'users' in j:
                j['users'] = None
            
            print( json.dumps(j, sort_keys=True, indent=2) + "\n----------------" )
        return rslt


    def newEvent(self,event):
        if args.verbose : 
            print("### creating TroopTrack event {} - {}".format(self.ttDate(event['start']),event['title']) )
        
        # make sure we can map the event type (data) to what TroopTrack will allow as event types
        if event['type'] not in self.event_types:
            errorExit("Event type '{}' not in TroopTrack event types : {}\n".format(event['type'], list(self.event_types.keys())) )

        # reformat \n in description into <p> for htmlism
        description = ""
        for p in event['description'].split("\n"):
            if len(p):                      # only non-blank lines
                if len(description):
                    description += "<p>"    # use <p> for newlines
                description += p

        # build the TroopTrack request structure
        eventInfo = {
                'title': event['title'],
                'event_type_id': self.event_types[event['type']],
                'start_at': self.ttDatetime(event['start']),
                'end_at': self.ttDatetime(event['end']),
                'location': event['where'],
                'description': description
            }

        ## prepare mark the things that's we'll need to fixup, since the API doesn't do right or give the option
        fixupFullday = fixupSelf = removeRSVP = False
        
        # full day and no TT API for it
        if 'fullday' in event:
            fixupFullday = True

        if event['type'] == 'Campout':
            campingNights = event['end']-event['start']
            eventInfo['camping_nights'] = campingNights.days
        
        if 'rsvpBy' in event:
            eventInfo['rsvp_deadline'] = self.ttDate(event['rsvpBy'])
        else:
            # can't turn off RSVP with API
            removeRSVP = True
        if 'invite' in event:
            eventInfo['send_invites_when'] = event['invite']
        if 'reminder' in event:
            eventInfo['send_reminder_when'] = event['reminder']

        eventInfo['inviteable_tokens'] = []
        if 'who' in event and len(event['who']) :
            for person in event['who'] :
                person = person.lower()
                if person in self.patrols:
                    eventInfo['inviteable_tokens'].append('{}'.format(self.patrols[person]))
                else:
                    if person in self.mailingLists:
                        eventInfo['inviteable_tokens'].append('MailingList-{}'.format(self.mailingLists[person]['id']))
                    else:
                        eventInfo['inviteable_tokens'] = ['User-{}'.format(self.userID)]        ## can't not specify one
                        warning("unknown person {}.  You'll need to fixup the event yourself".format(person, event))
    
        elif 'rsvp_deadline' in eventInfo or 'send_invites_when' in eventInfo or 'send_reminder_when' in eventInfo:
            ## can't not specify a 'who', so have to specify self in the TT API, then remove self later
            eventInfo['inviteable_tokens'].append('User-{}'.format(self.userID))
            fixupSelf = True

        eventInfo['guests_allowed']='no'
        eventInfo['payment_required_to_rsvp']='no'

        if args.verbose > 1:
            printDict('## EVENT INFO: ', eventInfo)

        ## make the request to create the event in the calendar
        r = self.ttRequest('events', data={'event':eventInfo}, headers={"Content-Type": "application/json"} )
        
        ## arrrgh, the TT API isn't quite good enough yet, go do fixups
        if fixupSelf or removeRSVP or fixupFullday:
            self.fixupEvent(r.json()['event']['event_id'], event, fixupSelf, removeRSVP, fixupFullday)

        
    def fixupEvent(self, eventID, event, fixupSelf, removeRSVP, fixupFullday):
        if args.verbose > 2 :
            print("#################################################################################")
            print("Fixups for event: {}({}) fixupSelf:{}, removeRSVP:{}, fixupFullday:{}".format(event['start'], event['title'], fixupSelf, removeRSVP, fixupFullday))

        tell = '''
            tell application "Safari"
                if not (exists document 1) then reopen
                delay 0.2
                tell document 1
                set URL to "https://troop466.trooptrack.com/plan/events/{0}/edit"
                    delay 3
                    -- remove RSVP checkbox if needed
                    if {1} then
                        do JavaScript "document.getElementById('event_rsvp_required').checked=false"
                    end if
                    -- remove the user checked (since it may have been added to complete the API event creation)
                    if {2} then
                        do JavaScript "document.getElementById('event_inviteable_tokens_user-{3}').checked=false"
                    end if
                    -- add the AllDay checkbox
                    if {4} then
                        do JavaScript "document.getElementById('event_all_day').checked=true"
                    end if
                    -- submit the changes
                    do JavaScript "document.getElementById('edit_event_{0}').submit()"
                    delay 3
                end tell
            end tell
        '''.format(eventID, removeRSVP, fixupSelf, self.userID, fixupFullday )
        if args.verbose > 2 :
            print(tell)
        doAppleScript(tell)
        
        
##########################################################################################

begin()

You are so much more eloquent in your style and coding than I. Thank a ton. If I add any useful pieces, I will be sure and post them back here.