Last week in the Legato Developers Corner, we hooked a user function onto a new menu item. Now we’re going to look at another way to add functionality to GoFiler by creating a script that runs automatically to record a log of each EDGAR filing made using GoFiler.
Friday, September 30. 2016
Legato Developers Corner #3: Creating an EDGAR Filing Log
Hooking new functions to the menu is a great way to customize the software but doesn’t always achieve the result you want. What if you want to improve or alter an existing menu function? You can use Legato to create user functions that happen before or after an existing function runs, or to even overwrite an existing function. This week, we’ll write a script that runs automatically after a user in GoFiler uses the File Live, File Test or File Test As Agent menu function and adds a new row into a very simple CSV file to record that these functions were used.
We will actually be building on this script for the next couple of weeks; this week, I’ll show you how to store very basic information. In the following weeks, we will modify the script to store more data, like the accession number of the filing. We will eventually discuss hooking it into a database system, and even retrieving a filing status from GoFiler’s mailbox and storing it as an additional field.
Like our script last week, this one is also a .ms file, so it runs at startup. It has the same three user functions as well: setup, main, and run. Obviously, the run function is going to be different, because this script has different functions. The setup function in this case is actually a lot simpler. Instead of adding a new menu function, it simply hooks onto an existing one. In this case, I’m actually hooking it to three different menu functions, to make sure I capture every EDGAR filing submitted via the software.
Just like last week, a couple of new concepts are included in the script below:
1. | Switch statements |
2. | Persistent settings |
3. | Reading and Writing CSV Files |
Our Sample Script
// GoFiler Legato Script - Record Filings // ------------------------------------------ // // Rev 9/20/2016 // // (c) 2016 Novaworks, LLC -- All rights reserved. // // // Notes: // - None. int setup (); int run (int f_id, string mode); // set up the script and add it to the menu bar. runs on startup automatically. int setup() { string fnScript; int rc; // get the name of this script file fnScript = GetScriptFilename(); // hook the "run" function of this script to execute when the new menu // option is activated. MenuSetHook("EDGAR_SUBMIT_LIVE", fnScript, "run"); MenuSetHook("EDGAR_SUBMIT_TEST", fnScript, "run"); MenuSetHook("EDGAR_SUBMIT_TEST_AGENT", fnScript, "run"); return ERROR_NONE; } // runs every time the menu button is clicked on int run(int f_id, string mode){ // test/live code. string f_code; // the actual data file. string data_file; // where the data file is stored on my PC. string data_file_path; // contents of a data file; string data_file_contents[][]; // index to start writing data; int index; // only execute on post-process of function. if(mode!="postprocess"){ return ERROR_NONE; } switch(f_id){ case MenuFindFunctionID("EDGAR_SUBMIT_LIVE"): f_code = "LIVE"; break; case MenuFindFunctionID("EDGAR_SUBMIT_TEST"): f_code = "TEST"; break; case MenuFindFunctionID("EDGAR_SUBMIT_TEST_AGENT"): f_code = "TEST (AGENT)"; break; } // get the location of the data file data_file_path = GetSetting("settings","filing_history"); // if we don't have a data file, ask where we should store it. if( IsFile(data_file_path) == false){ data_file_path = BrowseSaveFile("Select Filing Log Location", "CSV Files|*.csv","",0,"CSV Files|*.csv"); // if the user cancelled, stop here. if (GetLastError()==ERROR_CANCEL){ return ERROR_CANCEL; } // save name of storage file PutSetting("settings","filing_history",data_file_path); // set headings for CSV file data_file_contents[0][0]="Name"; data_file_contents[0][1]="Live/Test"; data_file_contents[0][2]="Date"; } else{ // get data from existing file data_file_contents = CSVReadTable(data_file_path); } // get index of where to put our information. index = ArrayGetAxisDepth(data_file_contents); // set the new data into the index data_file_contents[index][0] = GetUserName(); data_file_contents[index][1] = f_code; data_file_contents[index][2] = GetLocalTime(DS_DATE_AT_TIME); // write output and quit CSVWriteTable(data_file_contents,data_file_path); return ERROR_NONE; } // every script requires a main function when it's run from the IDE int main() { // make sure the function is set up. setup(); return ERROR_NONE; }
Switch Statements
Switch statements are a staple of many programming languages, and Legato is no different. The general idea is that you take a variable, and for each possible case, you define what action to take.
switch(f_id){ case MenuFindFunctionID("EDGAR_SUBMIT_LIVE"): f_code = "LIVE"; break; case MenuFindFunctionID("EDGAR_SUBMIT_TEST"): f_code = "TEST"; break; case MenuFindFunctionID("EDGAR_SUBMIT_TEST_AGENT"): f_code = "TEST (AGENT)"; break; }
The first line defines the switch and what variable is going to be switched on. Each case defines a specific condition (it doesn’t have to be a constant either; I have SDK functions in each of my possible cases!), and what happens for that condition. You can also define a default action that happens whenever none of the cases are met.
Persistent Settings
Often when running a script in Legato, you will want to save settings or information that a user has entered to prevent them from having to enter the same information each and every time. GoFiler makes saving and retrieving settings very easy with an SDK function named GetSetting and PutSetting.
// get the location of the data file data_file_path = GetSetting("settings","filing_history"); // save name of storage file PutSetting("settings","filing_history",data_file_path);
These functions can save and retrieve information from a given file. You can choose not to specify a file and GoFiler will automatically create a settings file named after the script in its local appdata area. In addition to the name of the file, a settings area is required (“settings” in this case), as is the name of the actual setting you want to get or store (“filing_history”). If you’re putting a setting into the file, a final parameter is required, which is just the value of the setting itself.
Reading and Writing CSV Files
One of the most common tasks you’ll perform using Legato is reading and writing files. When working with tables, CSV is a very simple and basic format that makes it quick and easy to access and store data.
The CSVReadTable SDK function takes a file path as a parameter and returns a two-dimensional array (a table) of all of the data that was contained inside of the file. The CSVWriteTable SDK function does the opposite; it takes a two-dimensional array and writes it out to a file.
// get data from existing file data_file_contents = CSVReadTable(data_file_path); } // get index of where to put our information. index = ArrayGetAxisDepth(data_file_contents); // set the new data into the index data_file_contents[index][0] = GetUserName(); data_file_contents[index][1] = f_code; data_file_contents[index][2] = GetLocalTime(DS_DATE_AT_TIME); // write output and quit CSVWriteTable(data_file_contents,data_file_path); return ERROR_NONE; }
So instead of reading and writing table data line by line, you can read an entire table in, modify exactly what you want to modify, and write it back out.
A Quick Word on Function Codes
Every menu function in GoFiler has a unique code, and it can be sometimes difficult to see exactly what a function’s code is. You can download an Excel file (.xls) that contains a list of every GoFiler menu function and its code by clicking here.
If you have questions about a menu function code, contact us at legato@novaworkssoftware.com and we’ll help you find it.
The function codes for the menu functions we will be using in our script are EDGAR_SUBMIT_LIVE, EDGAR_SUBMIT_TEST, and EDGAR_SUBMIT_AGENT.
Script Walkthrough
The structure of this script is very similar to the last one. We’ll start with the setup function.
// set up the script and add it to the menu bar. runs on startup automatically. int setup() { string fnScript; int rc; // get the name of this script file fnScript = GetScriptFilename(); // hook the "run" function of this script to execute when the new menu // option is activated. MenuSetHook("EDGAR_SUBMIT_LIVE", fnScript, "run"); MenuSetHook("EDGAR_SUBMIT_TEST", fnScript, "run"); MenuSetHook("EDGAR_SUBMIT_TEST_AGENT", fnScript, "run"); return ERROR_NONE; }
The setup function is simpler this time around. We just need to run the SetMenuHook SDK function three times to attach our script to the File Live, File Test, and File Test as Agent menu functions. The script is going to run the same user function when each of these menu functions are run, so the three MenuSetHook lines are the same except for the menu function codes.
The run user function is where everything is going to happen for this script. We used the mode parameter before, but this time we’re also going to use the f_id parameter. Whenever a hook that was set calls the run function in our script, it passes along the function code (f_id) of the menu function that called it. We can use this to determine exactly what called our script.
We start out with a couple simple variable declarations. Then we check the mode to see if we’re postprocess or not. We only want to record information after a filing is submitted, so if the mode is not postprocess, the script will exit. Once it’s confirmed we’re running postprocess, we have our switch to get the function code and test it against the codes for the menu functions we want. Once we get a match, we know one of our designated menu functions called this script. The run function then sets the f_code variable so we can save that information. Depending on which menu function called the script, f_code will be set to LIVE, TEST or TEST (AGENT).
// only execute on post-process of function. if(mode!="postprocess"){ return ERROR_NONE; } switch(f_id){ case MenuFindFunctionID("EDGAR_SUBMIT_LIVE"): f_code = "LIVE"; break; case MenuFindFunctionID("EDGAR_SUBMIT_TEST"): f_code = "TEST"; break; case MenuFindFunctionID("EDGAR_SUBMIT_TEST_AGENT"): f_code = "TEST (AGENT)"; break; }
Now that f_code is set, we can get the name of the data file, and write out to it.
// get the location of the data file data_file_path = GetSetting("settings","filing_history"); // if we don't have a data file, ask where we should store it. if( IsFile(data_file_path) == false){ data_file_path = BrowseSaveFile("Select Filing Log Location", "CSV Files|*.csv","",0,"CSV Files|*.csv"); // if the user cancelled, stop here. if (GetLastError()==ERROR_CANCEL){ return ERROR_CANCEL; } // save name of storage file PutSetting("settings","filing_history",data_file_path); // set headings for CSV file data_file_contents[0][0]="Name"; data_file_contents[0][1]="Live/Test"; data_file_contents[0][2]="Date"; } else{ // get data from existing file data_file_contents = CSVReadTable(data_file_path); }
The SDK function GetSetting is used to get the location of the data file. On the next line, we check if it actually points to a file that exists. If it’s not pointing to an existing file (either because the file was deleted, or because this is the first time running the script), the BrowseSaveFileSDK function is run, which asks the user to pick a location where the file will be saved. It is very important that the next line checks if the last error code was ERROR_CANCEL or not! If the user clicks the Cancel button on the browse dialog or closes it without picking a file path, we want to make sure that the script stops executing.
If the user did pick a file, we can use the PutSetting SDK function to save the location the user chose. Once the location is saved for next time, we initialize the heading row of our data file table. I added three headings: “Name”, ”Live/Test”, and “Date”.
If the user had already defined a file to write to, the script skips all that and reads the contents of the file using our CSVReadTable SDK function. Now we just need to add our new information to the table, and write it out.
First, we need to get the index in the table of the next available row since we don’t want to overwrite data that’s already there. The ArrayGetAxisDepth SDK function can tell us how many used rows there are in a given table. (It can actually tell us a lot more, but in this case that’s all we want. For more information on what other information ArrayGetAxisDepth can provide, see the Legato SDK Documentation).
// get index of where to put our information. index = ArrayGetAxisDepth(data_file_contents);
All arrays in Legato are zero-based, so the number of rows is equal to the first empty row in the table. Likewise, the first column will start at zero. We can use that index to set the first three columns of that row. Column A will be the username of the user who submitted the filing, which we can get using the GetUserName SDK function. Column B will be the f_code variable we set earlier that tell us the type of filing (Live, Test, or Test (Agent)) that was submitted. Column C will be the date and time the filing we submitted, which we’ll record using the GetLocalTime SDK function.
// set the new data into the index data_file_contents[index][0] = GetUserName(); data_file_contents[index][1] = f_code; data_file_contents[index][2] = GetLocalTime(DS_DATE_AT_TIME);
Once we’ve finished modifying our data table, we can just write it back out to our file location with the CSVWriteTable SDK function, and then return without an error. Done!
// write output and quit CSVWriteTable(data_file_contents,data_file_path); return ERROR_NONE; }
This is a very simple script, but we can do a lot more with it to collect more information when users file. We could store more information about the filing, like the accession number, or we can check to see if the filing was accepted or suspended. We can also write out the data we’re collecting to something more than a simple CSV file.
Next week, we’ll explore how we can get more information about our filing so our log contains additional data.
Steven Horowitz has been working for Novaworks for over five years as a technical expert with a focus on EDGAR HTML and XBRL. Since the creation of the Legato language in 2015, Steven has been developing scripts to improve the GoFiler user experience. He is currently working toward a Bachelor of Sciences in Software Engineering at RIT and MCC. |
Additional Resources