I'm designing a script that takes an object (jsonData[data]) and inputs its values into a different sheet based on which product it is.
Currently the script inputs all the data into a new row each time the form reaches a new stage, however the form goes through 4 stages of approval and so I'm finding each submission being entered into 4 different rows. Each submission has an "Id" value within the object which remains the same (but each submission could also be on any row in the sheet as it's used a lot).
I'm checking whether the ID exists in the sheet and using iteration to find the row number:
function updatePlatformBulkInfo(jsonData) {
var sheetUrl = "https://docs.google.com/spreadsheets/d/13U9r9Lu2Fq1WTT8pQ128heCm6_gMmH1R4O6u8e7kvBo/edit#gid=0";
var sheetName = "PlatformBulkSetup";
var doc = SpreadsheetApp.openByUrl(sheetUrl);
var sheet = doc.getSheetByName(sheetName);
var rowList = [];
var formId = jsonData["Id"];
var allSheetData = sheet.getDataRange().getValues();
setLog("AllSheetData = " + allSheetData[1][11]) //Logs to ensure data is collected correctly
var rowEdited = false;
var rowNumber = 0;
//Check whether ID exists in the sheet
for (var i = 0; i < allSheetData.length; i++) {
if(allSheetData[i][11] == formId) {
rowEdited = true;
} else {
rowNumber += 1;
}
}
My issue is with the next part:
//Append row if ID isn't duplicate or update row if duplicate found
if (rowEdited == false) {
for (var data in jsonData) {
rowList.push(jsonData[data])
}
setLog("***Row List = " + rowList + " ***");
setLog("***Current Row Number = " + rowNumber + " ***");
sheet.appendRow(rowList);
} else if(rowEdited == true){
var newRowValue = jsonData[data];
sheet.getRange(rowNumber, 1).setValues(newRowValue);
}
Everything works fine if the duplicate isn't found (the objects values are appended to the sheet). But if a duplicate is found I'm getting the error:
Cannot find method setValues(string)
This looks to me like i'm passing a string instead of an object, but as far as I'm aware I've already converted the JSON string into an object:
var jsonString = e.postData.getDataAsString();
var jsonData = JSON.parse(jsonString);
How can I modify my script to write the updated data to the matched row?
It's unclear based on your code whether or not you will actually write to the correct cell in the case of a duplicate. As presented, it looks as though you loop over the sheet data, incrementing a row number if the duplicate is not found. Then, after completing the loop, you write to the sheet, in the row described by rowNumber, even though your code as written changes rowNumber after finding a duplicate.
To address this, your loop needs to exit upon finding a duplicate:
var duplicateRow = null, checkedCol = /* your column to check */;
for(var r = 0, rows = allSheetData.length; r < rows; ++r) {
if(allSheetData[r][checkedCol] === formId) {
// Convert from 0-base Javascript index to 1-base Range index.
duplicateRow = ++r;
// Stop iterating through allSheetData, since we found the row.
break;
}
}
In both cases (append vs modify), you seem to want the same output. Rather than write the code to build the output twice, do it outside the loop. Note that the order of enumeration specified by the for ... in ... pattern is not dependable, so if you need the elements to appear in a certain order in the output, you should explicitly place them in their desired order.
If a duplicate ID situation is supposed to write different data in different cells, then the following two snippets will need to be adapted to suit. The general idea and instructions still apply.
var dataToWrite = [];
/* add items to `dataToWrite`, making an Object[] */
Then, to determine whether to append or modify, test if duplicateRow is null:
if(dataToWrite.length) {
if(duplicateRow === null) {
sheet.appendRow(dataToWrite);
} else {
// Overwriting a row. Select as many columns as we have data to write.
var toEdit = sheet.getRange(duplicateRow, 1, 1, dataToWrite.length);
// Because setValues requires an Object[][], wrap `dataToWrite` in an array.
// This creates a 1 row x N column array. If the range to overwrite was not a
// single row, a different approach would be needed.
toEdit.setValues( [dataToWrite] );
}
}
Below is the most basic solution. At the end of this post, I'll expand on how this can be improved. I don't know how your data is organized, how exactly you generate new unique ids for your records, etc., but let's assume it looks something like this.
Suppose we need to update the existing record with new data. I assume your JSON contains key-value pairs for each field:
var chris = {
id:2,
name: "Chris",
age: 29,
city: "Amsterdam"
};
Updating a record breaks down into several steps:
1) Creating a row array from your object. Note that the setValues() method accepts a 2D array as an argument, while the appendRow() method of the Sheet class accepts a single-dimension array.
2) Finding the matching id in your table if it exists. The 'for' loop is not very well-suited for this idea unless you put 'break' after the matching id value is found. Otherwise, it will loop over the entire array of values, which is redundant. Similarly, there's no need to retrieve the entire data range as the only thing you need is the "id" column.
IMPORTANT: to get the row number, you must increment the array index of the matching value by 1 as array indices start from 0. Also, if your spreadsheet contains 1 or more header rows (mine does), you must also factor in the offset and increment the value by the number of headers.
3) Based on the matching row number, build the range object for that row and update values. If no matching row is found, call appendRow() method of the Sheet class.
function updateRecord(query) {
rowData = [];
var keys = Object.keys(query);
keys.forEach(function(key){
rowData.push(query[key]);
})
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheets()[0];
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var idColumn = 1;
var ids = sheet.getRange(2, idColumn, sheet.getLastRow() - 1, 1).getValues();
var i = 0;
var matchedRow;
do {
if (ids[i] == query.id) { matchedRow = i + 2; }
i++;
} while (!matchedRow && i < ids.length);
if (matchedRow) {
var row = sheet.getRange(matchedRow, idColumn, 1, rowData.length);
row.setValues([rowData]);
} else {
sheet.appendRow(rowData);
}
}
NOTE: if your query contains only some fields that need to be updated (say, the 'id' and the 'name' field), the corresponding columns for these fields will be
headers.indexOf(query[key]) + 1;
Possible improvements
If the goal is to use the spreadsheet as a database and define all CRUD (Create, Read, Write, Delete) operations. While the exact steps are beyond the scope of the answer, here's the gist of it.
1) Deploy and publish the spreadsheet-bound script as a web app, with the access set to "anyone, even anonymous".
function doGet(e) {
handleResponse(e);
}
function doPost(e) {
handleRespone(e);
}
function handleResponse(e) {
if (e.contentLength == -1) {
//handle GET request
} else {
//handle POST request
}
}
2) Define the structure of your queries. For example, getting the list of values and finding a value by id can be done via GET requests and passing parameters in the url. Queries that add, remove, or modify data can be sent as payload via POST request. GAS doesn't support other methods besides GET and POST, but you can simulate this by including relevant methods in the body of your query and then selecting corresponding actions inside handleResponse() function.
3) Make requests to the spreadsheet URL via UrlFetchApp. More details on web apps https://developers.google.com/apps-script/guides/web
Is there an eloquent way, more or less, to get the last displayed record in a grid in Acumatica? Let's say even if they do all the sorting and rearranging, is there a way for example when pressing a button on a grid to get the last record? Basically, I would like to copy that record as a new one.
Create a PXAction for your button.
Inside the PXAction iterate in your data view until the last record.
For example, if the name of your Data view Bound to your grid is YzLines, and object type in the grid line (DAC) is Yz, then it can be:
Yz lastLine;
foreach (Yz line in YzLines.Select())
lastLine = line;
To get to the last record you can also use .Last() or .LastOrDefault().
If you need the last record according to client sorting, you should implement a data view delegate, it looks like this:
protected virtual IEnumerable yzLines()
{
PXSelectBase<Yz> cmd =
new PXSelectJoinGroupBy<Yz, ...>(this);
int startRow = PXView.StartRow; //Get starting row of the current page
int totalRows = 0;
foreach (PXResult<Yz> res in
cmd.View.Select(null, null,
PXView.Searches,
ARDocumentList.View.GetExternalSorts(),//Get sorting fields
ARDocumentList.View.GetExternalDescendings(),//Get sorting direction
ARDocumentList.View.GetExternalFilters(),//Get filters
ref startRow,
PXView.MaximumRows, //Get count of records in the page
ref totalRows))
{
//processing of records
}
PXView.StartRow = 0;//Reset starting row
}
I have an Access database which I need to retrieve all fields except the first and last and display it in a JTable. Everything works perfectly fine when I create my Object[][] but when i return it, i get a NullPointerException. I tried to find where there could be a null value in the database by printing the whole object out but that works fine and no values are null. Why would returning the Object[][] give me a NullPointerException and how can i fix it?
the stack trace is:
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
public Object [] [] SetTrainingLogTable() throws SQLException
{
DatabaseConnection connection = new DatabaseConnection();
//Retrieves all the data from the TrainingLog table
ResultSet resultset = connection.SelectStatements("SELECT * FROM TrainingLog");
//Retrieves the number of entries
ResultSet numberofworkouts = connection.SelectStatements("SELECT COUNT(*) FROM TrainingLog");
int count = numberofworkouts.getInt(1);
number = count;
String[][] table = new String [count] [6];
//Number to incriment for while loops
int row = 0;
String date = "";
while(row<count)
{
date = resultset.getString(2);
table [row][0] = calculate.RefineDate(date);
table [row][1] = resultset.getString(3);
table [row][2] = resultset.getString(4);
table [row][3] = resultset.getString(5);
table [row][4] = resultset.getString(6);
table [row][5] = resultset.getString(7);
resultset.next();
row++;
}
Object[][] data = table;
connection.close();
return data;
}
I ran a debugger and it only gives the error when the return line is run.
It's best to post the stack trace and tell which line is raising the error. However, the typical way of writing such code is:
Connection con = ...;
Statement st = ...;
ResultSet rs = ...;
while (rs.next()) {
// ...
}
The result set starts out pointing before the first row. rs.next() returns whether there is a next row, and advances to it if it exists. Can you rewrite it in that style?
Other suggestions:
Can you create an actual object type instead of using Object[] to store the data from each row? Call it Workout.
Can you use a List<Workout> instead of your Object[][]?
Is the date stored in the database as a SQL DATE or TIMESTAMP? Then, don't convert it to a Java String: use java.sql.Date or java.util.Date. At work, I have a large old program that uses strings for dates, and it uses different formats to convert the values at different times. It's pretty miserable.
Don't use SELECT *. Give the names of the columns to return. Use the rs.getString("column_name") syntax.
There's no need to set one variable to the returned table and immediately set another variable to it.
Closing the connection or statement should be done in a finally block, or by try-with-resources.
I have come across a problem when using the BaseFieldControl that is driving me to distraction.
Essentinally I am trying to convert a list into a HTML table using the BaseFieldControl to render the fields.
When my table renders it writes out the correct number of lines BUT the data in each line is always the same as the first item in the list.
When I change the ControlMode property from SPControlMode.Display to SPControlMode.Edit the list renders correctly ( apart from being in Edit mode )
When my code running with ControlMode set to SPControlMode.Display I can actually get at the correct value in the BaseFieldControl.ItemFieldValue property but the wretched BaseFieldControl still insists on rendering the first item in the list!
I've also installed the web part on a SharePoint foundation and SharePoint 2010 server and I get the same results!
Finally I've googled around and found other peoples examples. Unfortunately when I try other dev's code ( unmodified ) I get exactly the same results!
This is what I'm doing. Any suggestions would be really appreciated!
foreach (string f in list.DefaultView.ViewFields)
{
TableCell c = new TableCell();
var i = item[f];
if (i != null)
{
SPField spf = item.Fields.GetField(f);
BaseFieldControl bfc = spf.FieldRenderingControl;
bfc.ControlMode = SPControlMode.Display;
bfc.Value = bfc.ItemFieldValue;
bfc.ID = Guid.NewGuid().ToString();
bfc.FieldName = spf.Title;
bfc.ListId = list.ID;
bfc.ItemId = item.ID;
SPContext context = SPContext.GetContext(this.Context, item.ID, list.ID, SPContext.Current.Web);
bfc.ItemContext = context;
bfc.RenderContext = context;
bfc.EnableViewState = true;
bfc.Visible = true;
c.Controls.Add(bfc);
}
else
{
c.Text = "NULL";
}
r.Cells.Add(c);
}
I finally fixed it. Turns out to be a problem with the SPWeb object. I had grabbed it from SPContext and passed it through as a reference to my method.
When I stopped doing that but instead created it within the method ( and created it once per item in the list ) it all worked fine.
Very strange.
I have some code that inserts a list item into a list...
I then have this code
SPFolder folder = web.Folders["Lists"].SubFolders[list.RootFolder.Name].SubFolders["Attachments"].SubFolders[item.ID.ToString()];
foreach (SPFile file in folder.Files)
{
string attachmentName = this.downloadedMessageID + ".xml";
if (file.Name == attachmentName)
{
SPFieldUrlValue value = new SPFieldUrlValue();
value.Description = this.downloadedMessageID + ".xml";
value.Url = this.SiteAddress + file.Url;
item["ZFO"] = value;
}
}
this is fine except for one problem... before this code actually works... I need to call the item.update() method to save the item to SharePoint...
But as you can see there is more work to do ... after item.update is called...
So this means... I have
work
item.update();
more work
item.update();
The problem I am having is I really want just
work
item.update();
So that in any event of failure the whole thing will fail at once or pass at once.... (almost like a SQL transaction).
So whats preventing me from doing this is - I need to set a hyperlink to one of the fields in the list item, this will be to an attachment in the list attachments collection.
Is there any way I can predict this address without having saved the list item to MOSS?
An attachment path depends on the item ID, and I don't believe your item will have an ID until you save it. Have you considered storing the attachments in a document library instead, linked by the field you're trying to set?
Transactional operation isn't exactly SharePoint's strong suit.