IndexedDB wildcard at start and end of searchterm - wildcard

This topic explains how to use wildcards ad the end of the searchterm using IndexedDB.
I am looking for a way to add a wildcard at the end AND at the start of the searchterm.
In SQL it would be: LIKE '%SearchTerm%'.
How can I achieve this with IndexedDB? Here is my code:
function getMaterials() {
var materialNumber = $("#input").val();
var transaction = db.transaction(["materials"]);
var objectStore = transaction.objectStore("materials");
var request = objectStore.openCursor(IDBKeyRange.bound(materialNumber, materialNumber + '\uffff'), 'prev');
$("#output").find("tr:gt(0)").remove();
request.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
var newRow = '<tr><td>'+ cursor.value.materialNumber +'</td>'+
'<td>'+ cursor.value.description +'</td>'+
'<td>'+ cursor.value.pieces +'</td>'+
'<td>'+ cursor.value.price +'</td></tr>';
$('#output').append(newRow);
cursor.continue();
}
};
};
EDIT:
I could achieve this by letting indexDB return all rows and then narrow down in JavaScript. But there must be a better approach in terms of performance.
if (cursor.value.materialNumber.indexOf(materialNumber) != -1){
//add result...
}

This was not how idb was intended to be used. If you want text searching, parse text into tokens, store the tokens, use an index on the tokens, and do lookups on the index to get a pointer to the full text.

Related

How to maintain a session with .aspx server while web scraping through pagination?

I am unable to maintain a session with a .aspx server. I am trying to scrape data by paginating, but it keeps telling me "The Results have expired. Please resubmit the search." I have tried maintaining cookies so I don't think that is the problem unless I somehow did it wrong?
I have to navigate through by first making a GET request to the following URL:
https://www.wandsworth.gov.uk/planning-and-building-control/search-planning-applications/
The following is the code I use to make the request.
First these are all my requires
const cheerio = require('cheerio');
const url = require('url');
const rp = require('request-promise');
const ss = require('string-similarity');
const tc = require('tough-cookie');
Here is how I make my request
var options = {
uri: 'https://www.wandsworth.gov.uk/planning-and-building-control/search-planning-applications/',
transform: function(body){ return cheerio.load(body) },
method: 'GET'
}
var $ = await rp(options);
Now I extract the information I need in order to make a successful post request, and I use the 'string-similarity' package to find a select element that closely matches a tag that matches my input.
// Extract selectable elements
var obj_collection = $('#cboStreetReferenceNumber')[0].children;
var collection = []; // array of inner strings for each select element
// Push innerHTML strings to collection
for(let i=0; i<obj_collection.length; i++){
try {
collection.push(obj_collection[i].children[0].data);
} catch(e) {
collection.push('');
}
}
// Find the best match for our given address
var matches = ss.findBestMatch(address, collection);
var cboStreetReferenceNumber=
obj_collection[matches.bestMatchIndex].attribs.value;
// These are used to verify us
var __VIEWSTATE = $('#__VIEWSTATE')[0].attribs.value;
var __VIEWSTATEGENERATOR = $('#__VIEWSTATEGENERATOR')[0].attribs.value;
var __EVENTVALIDATION = $('#__EVENTVALIDATION')[0].attribs.value;
var cboMonths = 1;
var cboDays = 1;
var csbtnSearch = 'Select';
var rbGroup = 'rbNotApplicable';
// Modify options
options.uri = $('#M3Form')[0].attribs.action;
options.method = 'POST';
options.form = {
cboStreetReferenceNumber,
__VIEWSTATE,
__VIEWSTATEGENERATOR,
__EVENTVALIDATION,
cboMonths,
cboDays,
csbtnSearch,
rbGroup
};
options.followAllRedirects = true;
options.resolveWithFullResponse = true;
delete options.transform;
Now with these options, I'm ready to make my request to page 1 of the data I'm looking for.
// method: #POST
// link: "Planning Explorer"
var body = await rp(options);
var $ = cheerio.load(body.body);
console.log(body.request);
var Referer = 'https://planning1.wandsworth.gov.uk' + body.req.path;
var scroll_uri = 'https://planning1.wandsworth.gov.uk/Northgate/PlanningExplorer/Generic/StdResults.aspx?PT=Planning%20Applications%20On-Line&PS=10&XMLLoc=/Northgate/PlanningExplorer/generic/XMLtemp/ekgjugae3ox3emjpzvjtq045/c6b04e65-fb83-474f-b6bb-2c9d4629c578.xml&FT=Planning%20Application%20Search%20Results&XSLTemplate=/Northgate/PlanningExplorer/SiteFiles/Skins/Wandsworth/xslt/PL/PLResults.xslt&p=10';
options.uri = scroll_uri;
delete options.form;
delete options.followAllRedirects;
delete options.resolveWithFullResponse;
options.method = 'GET';
options.headers = {};
options.headers.Referer = Referer;
options.transform = function(body){
return cheerio.load(body);
}
var $ = await rp(options);
Once I get the next page, I am given a table with 10 items and some pagination if there are more than 10 items available based on my POST request.
This all goes fine until I try to paginate to page 2. The resulting HTML body tells me that my search has expired and that I need to resubmit a search. That means going back to step 1 and submitting a POST request again, however that will always bring me to page 1 of the pagination.
Therefore, I need to somehow find a way to maintain a connection with this server while I 'scroll' through its pages.
I am using node.js & request-promise to make my requests.
The following is my code:
I have already tried maintaining cookies between requests.
Also, __VIEWSTATE shouldn't be the problem because the request to page 2 should be a GET request.
I was able to find a workaround by using the headless browser "Puppeteer" in order to maintain a connection with the server. However, I still do not know how to solve this problem by making raw requests.

Appmaker Query on Calculated Records

I'm sure I'm doing something wrong... but every time I query on a calculated datasource, I get the error "cannot handle returning cyclic object."
Here's the gist:
I have a calculated model that fetches a user's google contacts and places the full name field into a table on the UI. The goal is to have a separate text box that can be used to search the full name field and then repopulate the table on the same page with the results of the search, similar to how google contacts search behavior works. The on value change event of the text box sends the textbox value to this server script:
function searchContacts (sq) {
var ds = app.models.Contacts.newQuery();
ds.filters.FullName._contains = sq;
var results = ds.run();
return results;
}
But every time I get the cyclic object error when the values are returned from that function. The error actually fires when the query run command (ds.run) is executed.
I've tried querying the datasource as well, but I've read somewhere that you can't query the datasource of a calculated model because it doesn't exist, so you have to query the model.
Any help would be much appreciated.
From your question it is not 100% clear, what you are trying to do. In case you are actually using Calculated Model, then your Server Script Query should look like this:
var sq = query.parameters.SearchQuery;
var contactsQuery = app.models.Contacts.newQuery();
contactsQuery.filters.FullName._contains = sq;
var contacts = ds.run();
var results = contacts.map(function(contact) {
var calcRecord = app.MyCalcModel.newRecord();
calcRecord.Name = contact.FullName;
return calcRecord;
});
return results;
Note, that you cannot return objects of arbitrary type from Server Script Query, only of type of this particular Calculated Model.
But from some parts of your description and error text if feels like you are trying to load records with async serever call using google.scritp.run. In this case you cannot return App Maker records(App Script doesn't allow this) and you need to map them to simple JSON objects.
I don't think I was super-clear on my original post.
I have a calculated model that is all of the user's contacts from Google Contacts (full name, email, mobile, etc...) On the UI I have a list widget that's populated with all of the Full Name fields and above the list widget a text input that's used to search the list widget. So the search text box's on input change event sends a request to query the Full Names, similar to how Google Contact's search feature works.
Screen Shot
It appears that App Maker doesn't let you query calculated models, so I have this workaround - unless someone comes up with something better:
This is the onInputChange handler for the search text box:
sq = app.pages.SelectClient.descendants.TextBox1.value;
app.datasources.SearchContacts.query.parameters.Name = sq;
app.datasources.SearchContacts.load();
This is the Server Script Code (thanks to #Pavel Shkleinik for the heads up):
var sq = query.parameters.Name;
if (sq !== null) {
return getContactsbyName(sq);
} else {
return getContacts();
}
And the server code with no query:
function getContacts() {
var results = [];
var contacts = ContactsApp.getContacts();
contacts.forEach(function(item) {
var contact = app.models.Contacts.newRecord();
contact.FullName = item.getFullName();
var emails = item.getEmails(ContactsApp.Field.WORK_EMAIL);
if (emails.length > 0) {
contact.PrimaryEmail = emails[0].getAddress();
}
contact.LastName = item.getFamilyName();
contact.FirstName = item.getGivenName();
var phones = item.getPhones(ContactsApp.Field.MOBILE_PHONE);
if (phones.length > 0) {
contact.Mobile = phones[0].getPhoneNumber();
}
var addresses = item.getAddresses(ContactsApp.Field.WORK_ADDRESS);
if (addresses.length > 0) {
contact.Address = addresses[0].getAddress();
}
results.push(contact);
results.sort();
});
return results;
}
And with the query:
function getContactsbyName(sq) {
var results = [];
var contacts = ContactsApp.getContactsByName(sq);
contacts.forEach(function(item) {
var contact = app.models.Contacts.newRecord();
contact.FullName = item.getFullName();
var emails = item.getEmails(ContactsApp.Field.WORK_EMAIL);
if (emails.length > 0) {
contact.PrimaryEmail = emails[0].getAddress();
}
contact.LastName = item.getFamilyName();
contact.FirstName = item.getGivenName();
var phones = item.getPhones(ContactsApp.Field.MOBILE_PHONE);
if (phones.length > 0) {
contact.Mobile = phones[0].getPhoneNumber();
}
var addresses = item.getAddresses(ContactsApp.Field.WORK_ADDRESS);
if (addresses.length > 0) {
contact.Address = addresses[0].getAddress();
}
results.push(contact);
results.sort();
});
return results;
}
This way, the list populates with all of the names when there's no search query present, and then re-populates with the search query results as needed.
The only issue is that the call to the Google Contacts App to populate the Calculated Model is sometimes very slow.

highlightselection function in rangy overrides previous getselection

I'm using Rangy for highlighting text and stumbled upon a problem when calling the highlightSelection function.
highlightSelection: function(className, options) {
var converter = this.converter;
var classApplier = className ? this.classAppliers[className] : false;
options = createOptions(options, {
containerElementId: null,
selection: api.getSelection(this.doc),
exclusive: true
});
var containerElementId = options.containerElementId;
var exclusive = options.exclusive;
var selection = selection || options.selection;
var doc = selection.win.document;
var containerElement = getContainerElement(doc, containerElementId);
if (!classApplier && className !== false) {
throw new Error("No class applier found for class '" + className + "'");
}
// Store the existing selection as character ranges
var serializedSelection = converter.serializeSelection(selection, containerElement);
// Create an array of selected character ranges
var selCharRanges = [];
forEach(serializedSelection, function(rangeInfo) {
selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) );
});
var newHighlights = this.highlightCharacterRanges(className, selCharRanges, {
containerElementId: containerElementId,
exclusive: exclusive
});
// Restore selection
converter.restoreSelection(selection, serializedSelection, containerElement);
return newHighlights;
},
It looks like the selection object is being overridden with another call to getSelection().
What's the best way to stop it from doing that?
After doing further research, I came a cross an update by the creator of Rangy, to specifically address this issue. So,
Download the latest version of the files and make sure this is what you have in rangy-highlighter.js file under highlightSelection: function:
options = createOptions(options, {
containerElementId: null,
exclusive: true
});
var containerElementId = options.containerElementId;
var exclusive = options.exclusive;
var selection = options.selection || api.getSelection(this.doc);
var doc = selection.win.document;
var containerElement = getContainerElement(doc, containerElementId);
call the highlightSelection function like:
'highlighter.highlightSelection("highlight", {selection: sel});'
So you're setting your selection key with the value sel. 'selection' is just the name of the key expected by this function (read the github docs for more options and information) and sel should be the object your are trying to highlight and be called prior like:
'sel = rangy.getSelection();'
I am building a custom tool tip when someone highlights text, and I came across this issue. The way I solved it, was by creating a global variable range, and setting it to rangy.getSelection().getRangeAt(0). This will get you the range object for the selection, afterwards you can set the selection back to your saved value like this: rangy.getSelection().addRange(this.range)

Have knockout observable check for null

I was wondering if there was a way to have knockout check to see if data is null before it tries to put it into an observable?
Right now I do this:
if (!data.Filename) {
this.FileName = ko.observable("");
}
else {
this.FileName = ko.observable(data.Filename);
}
Otherwise a null value in the data will cause the entire property not to show up. Is there a way to use extenders or something that I can add a null check to without having to do this with every property? My data has nulls in random places that I can't control and I don't want the property not to show up because one row in the dataset has a null value for that property.
Seems like there should be a better way to do this.
heh
There are a number of ways to do this. What I would do is
var self = this;
self.fileName = ko.observable(data.Filename);
self.fileNameComputed = ko.computed(function(){
return self.fileName() || ""
});
Then, in your mark up reference the computed instead if the observable.
In Javascript there are other patterns available to do this.
The first, and simplest, is akin to the ?? operator in C#:
function ViewModel(data) {
data = data || {};
this.Filename .observable(data.Filename || "");
}
The || operator will return the left operand unless it is falsy, then it'll fall back to the second argument. My example above will:
Make sure data itself is "at least" an empty object (where .Filename would be undefined);
Make sure that the input to ko.observable(...) is "at least" the empty string.
A second option would be to use default options. An example that utilizes jQuery to merge input data and default options:
var defaultData = {
Filename: "enter-a-file" // could also be empty string of course!
};
function ViewModel(data) {
var dto = $.extend({}, defaultData, data);
this.Filename = ko.observable(dto.Filename);
}
This will "fold" data into defaultData, and fold that result into an empty, fresh object, making sure the dto variable holds the merged result. The rest of your function can then safely assume a fully populated input variable.
The third and final option I'll mention is a remix of QBM5's answer, but I agree with the commenter's there that if you can use a pureComputed (which, in your example, is perfectly fine), you probably should:
function ViewModel(data) {
var self = this;
data = data || {};
this.Filename = ko.observable(data.Filename);
this.FilenameText = ko.pureComputed(function() {
return self.Filename() || "";
});
}
PS. You didn't have the underlying issue you mention because you spell FileName and Filename with different capitalization on this and data respectively, didn't you? ;-)

Google maps Autocomplete: output only address without country and city

I use Places library to autocomplete address input. Search is limited to only one city, and I get output like this:
"Rossiya, Moskva, Leninskiy prospekt 28"
How to hide "Rossiya, Moskva"? ...
My query:
function() {
// Search bounds
var p1 = new google.maps.LatLng(54.686534, 35.463867);
var p2 = new google.maps.LatLng(56.926993, 39.506836);
self.options = {
bounds : new google.maps.LatLngBounds(p1, p2),
componentRestrictions: {country: 'ru'},
};
var elements = document.querySelectorAll('.address');
for ( var i = 0; i < elements.length; i++) {
var autocomplete = new google.maps.places.Autocomplete(elements[i],
self.options);
}
You can but you have to replace the value of the input field in two places.
Example:
var autocomplete = new google.maps.places.Autocomplete(input, placesOptions);
var input = document.getElementById('searchTextField');
inside the 'place_changed' event you need to do the following:
placeResult = autocomplete.getPlace();
//This will get only the address
input.value = placeResult.name;
This will change the value in the searchtextfield to the street address.
The second place is a bit tricky:
input.addEventListener('blur', function(){
// timeoutfunction allows to force the autocomplete field to only display the street name.
if(placeResult){ setTimeout(function(){ input.value = placeResult.name; }, 1); } });
The reason why we have to do this is because if you only add the event listener for blur, google places will populate the input field with the full address, so you have to 'wait' for google to update and then force your change by waiting some miliseconds.
Try it without the setTimeout function and you will see what I mean.
EDIT
You can't. I had it the other way around, that you were just looking for a city. There is no way to only print out the street name (I'm assuming that's a street name) from the address component.
OPPOSITE OF WHAT WAS ASKED
From the docs:
the (cities) type collection instructs the Place service to return results that match either locality or administrative_area3.
var input = document.getElementById('searchTextField');
var options = {
bounds: defaultBounds,
types: ['(cities)']
};
autocomplete = new google.maps.places.Autocomplete(input, options);
in result u have hash and from it u can get part what u want:
google.maps.event.addListener(autocomplete, 'place_changed', function() {
var place = autocomplete.getPlace();
now from "place" u can get it
place.geometry.location.lat()
and for address
place.address_components[0] or place.address_components[1] ...
depends on what u want to get
I had a very similar problem which indeed was solvable. This in an Angular 2 project but it should be applicable elsewhere as well. I filter my results for establishments, and wanted to show only the name and hide the address part of the result. This did the trick for me, a function executing once you select a suggestion:
getAddress(place: Object) {
this.zone.run(() => {
this.establishment = place['name'];
});
where zone is an NgZone component injected in the constructor and this.establishment is the variable tied to [(NgModel)] in the input field.
Inside place_changed set a timeout function:
var streetString = place.address_components[0] or place.address_components[1];
window.setTimeout(function() {
$('input').val(streetString);
}, 200);
This solution worked for me.

Resources