- Refactor Zotero.Translate save process. There are now itemSaving and itemDone handlers. The itemSaving handler is called as soon as the translator returns an item, but before it's saved to the database. The itemDone handler is called when the item is saved. This allows us to show a grayed-out item icon in the connector while the item is saving to server, and then un-gray it when it is saved to the server.
- Move Zotero.randomString() to Zotero.Utilities - Fix import of collections (broken by recent commits)
This commit is contained in:
parent
ea5a94e860
commit
1774962de9
7 changed files with 189 additions and 121 deletions
|
@ -330,7 +330,7 @@ var Zotero_File_Interface = new function() {
|
||||||
*/
|
*/
|
||||||
function _importDone(obj, worked) {
|
function _importDone(obj, worked) {
|
||||||
// add items to import collection
|
// add items to import collection
|
||||||
_importCollection.addItems(obj.newItems);
|
_importCollection.addItems([item.id for each(item in obj.newItems)]);
|
||||||
|
|
||||||
Zotero.DB.commitTransaction();
|
Zotero.DB.commitTransaction();
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
Zotero.Translate.ItemSaver = function(libraryID, attachmentMode, forceTagType) {
|
Zotero.Translate.ItemSaver = function(libraryID, attachmentMode, forceTagType) {
|
||||||
this.newItems = [];
|
this.newItems = [];
|
||||||
|
|
||||||
this._itemsToSaveToServer = [];
|
|
||||||
this._timeoutID = null;
|
this._timeoutID = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,37 +34,46 @@ Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD = 1;
|
||||||
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE = 2;
|
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE = 2;
|
||||||
|
|
||||||
Zotero.Translate.ItemSaver.prototype = {
|
Zotero.Translate.ItemSaver.prototype = {
|
||||||
"saveItem":function(item) {
|
/**
|
||||||
|
* Saves items to Standalone or the server
|
||||||
|
*/
|
||||||
|
"saveItems":function(items, callback) {
|
||||||
// don't save documents as documents, since we can't pass them around
|
// don't save documents as documents, since we can't pass them around
|
||||||
for(var i in item.attachments) {
|
var nItems = items.length;
|
||||||
if(item.attachments[i].document) {
|
for(var i=0; i<nItems.length; i++) {
|
||||||
item.attachments[i].url = item.attachments[i].document.location.href;
|
var attachments = item[i].attachments;
|
||||||
delete item.attachments[i].document;
|
var nAttachments = attachments.length;
|
||||||
|
for(var j=0; j<nAttachments.length; j++) {
|
||||||
|
if(attachments[j].document) {
|
||||||
|
attachments[j].url = attachments[j].document.location.href;
|
||||||
|
delete attachments[j].document;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// save items
|
|
||||||
this.newItems.push(item);
|
|
||||||
var me = this;
|
var me = this;
|
||||||
Zotero.Connector.callMethod("saveItems", {"items":[item]}, function(success) {
|
// first try to save items via connector
|
||||||
if(success === false && !Zotero.isFx) {
|
Zotero.Connector.callMethod("saveItems", {"items":items}, function(success, status) {
|
||||||
// attempt to save to server on a timer
|
if(success !== false) {
|
||||||
if(me._timeoutID) clearTimeout(me._timeoutID);
|
Zotero.debug("Translate: Save via Standalone succeeded");
|
||||||
me._itemsToSaveToServer.push(item);
|
callback(true, items);
|
||||||
me._timeoutID = setTimeout(function() { me._saveToServer() }, 2000);
|
} else if(Zotero.isFx) {
|
||||||
|
callback(false, new Error("Save via Standalone failed with "+status));
|
||||||
|
} else {
|
||||||
|
me._saveToServer(items, callback);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
"_saveToServer":function() {
|
/**
|
||||||
|
* Saves items to server
|
||||||
|
*/
|
||||||
|
"_saveToServer":function(items, callback) {
|
||||||
const IGNORE_FIELDS = ["seeAlso", "attachments", "complete"];
|
const IGNORE_FIELDS = ["seeAlso", "attachments", "complete"];
|
||||||
|
|
||||||
// clear timeout, since saving has begin
|
|
||||||
this._timeoutID = null;
|
|
||||||
|
|
||||||
var newItems = [];
|
var newItems = [];
|
||||||
for(var i in this._itemsToSaveToServer) {
|
for(var i in items) {
|
||||||
var item = this._itemsToSaveToServer[i];
|
var item = items[i];
|
||||||
|
|
||||||
var newItem = {};
|
var newItem = {};
|
||||||
newItems.push(newItem);
|
newItems.push(newItem);
|
||||||
|
@ -180,15 +188,14 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
|
|
||||||
var url = 'users/%%USERID%%/items?key=%%APIKEY%%';
|
var url = 'users/%%USERID%%/items?key=%%APIKEY%%';
|
||||||
var payload = JSON.stringify({"items":newItems}, null, "\t")
|
var payload = JSON.stringify({"items":newItems}, null, "\t")
|
||||||
this._itemsToSaveToServer = [];
|
|
||||||
|
|
||||||
Zotero.OAuth.doAuthenticatedPost(url, payload, function(status, message) {
|
Zotero.OAuth.doAuthenticatedPost(url, payload, function(status, message) {
|
||||||
if(!status) {
|
if(!status) {
|
||||||
Zotero.Messaging.sendMessage("saveDialog_error", status);
|
Zotero.debug("Translate: Save to server failed with message "+message+"; payload:\n\n"+payload);
|
||||||
Zotero.debug("Translate: Save to server payload:\n\n"+payload);
|
callback(false, new Error("Save to server failed with "+message));
|
||||||
Zotero.logError(new Error("Translate: Save to server failed: "+message));
|
|
||||||
} else {
|
} else {
|
||||||
Zotero.debug("Translate: Save to server complete");
|
Zotero.debug("Translate: Save to server complete");
|
||||||
|
callback(true, newItems);
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -465,11 +465,21 @@ Zotero.Server.Connector.SaveItem.prototype = {
|
||||||
// save items
|
// save items
|
||||||
var itemSaver = new Zotero.Translate.ItemSaver(libraryID,
|
var itemSaver = new Zotero.Translate.ItemSaver(libraryID,
|
||||||
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD, 1);
|
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD, 1);
|
||||||
for each(var item in data.items) {
|
itemSaver.saveItems(data.items, function(returnValue, data) {
|
||||||
var savedItem = itemSaver.saveItem(item);
|
if(returnValue) {
|
||||||
if(collection) collection.addItem(savedItem.id);
|
try {
|
||||||
}
|
for each(var item in data) {
|
||||||
sendResponseCallback(201);
|
if(collection) collection.addItem(savedItem.id);
|
||||||
|
}
|
||||||
|
sendResponseCallback(201);
|
||||||
|
} catch(e) {
|
||||||
|
sendResponseCallback(500);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendResponseCallback(500);
|
||||||
|
throw data;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -105,17 +105,32 @@ Zotero.Translate.Sandbox = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var newItem = translate._itemSaver.saveItem(item);
|
// We use this within the connector to keep track of items as they are saved
|
||||||
|
if(!item.id) item.id = Zotero.Utilities.randomString();
|
||||||
|
|
||||||
// Allow progress meter to update
|
if(translate instanceof Zotero.Translate.Web) {
|
||||||
//
|
// For web translators, we queue saves
|
||||||
// This can probably be re-enabled for web translators once badly asynced ones are fixed
|
translate.saveQueue.push(item);
|
||||||
if(!translate.noWait && translate instanceof Zotero.Translate.Import) {
|
translate._runHandler("itemSaving", item);
|
||||||
Zotero.wait();
|
} else {
|
||||||
|
var newItem;
|
||||||
|
translate._itemSaver.saveItems([item], function(returnValue, data) {
|
||||||
|
if(returnValue) {
|
||||||
|
newItem = data[0];
|
||||||
|
translate.newItems.push(newItem);
|
||||||
|
} else {
|
||||||
|
translate.complete(false, data);
|
||||||
|
throw data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow progress meter to update
|
||||||
|
if(!translate.noWait) Zotero.wait();
|
||||||
|
|
||||||
|
translate._runHandler("itemSaving", item);
|
||||||
|
// pass both the saved item and the original JS array item
|
||||||
|
translate._runHandler("itemDone", newItem, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// pass both the saved item and the original JS array item
|
|
||||||
translate._runHandler("itemDone", newItem, item);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -948,6 +963,30 @@ Zotero.Translate.Base.prototype = {
|
||||||
this.decrementAsyncProcesses();
|
this.decrementAsyncProcesses();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executed when items have been saved (which may happen asynchronously, if in connector)
|
||||||
|
*
|
||||||
|
* @param {Boolean} returnValue Whether saving was successful
|
||||||
|
* @param {Zotero.Item[]|Error} data If returnValue is true, this will be an array of
|
||||||
|
* Zotero.Item objects. If returnValue is false, this will
|
||||||
|
* be a string error message.
|
||||||
|
*/
|
||||||
|
"itemsSaved":function(returnValue, data) {
|
||||||
|
if(returnValue) {
|
||||||
|
// trigger deferred itemDone events
|
||||||
|
var nItems = data.length;
|
||||||
|
for(var i=0; i<nItems; i++) {
|
||||||
|
this._runHandler("itemDone", data[i], this.saveQueue[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveQueue = [];
|
||||||
|
} else {
|
||||||
|
Zotero.logError(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._runHandler("done", returnValue);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executed on translator completion, either automatically from a synchronous scraper or as
|
* Executed on translator completion, either automatically from a synchronous scraper or as
|
||||||
* done() from an asynchronous scraper. Finishes things up and calls callback function(s).
|
* done() from an asynchronous scraper. Finishes things up and calls callback function(s).
|
||||||
|
@ -1008,7 +1047,14 @@ Zotero.Translate.Base.prototype = {
|
||||||
if(returnValue === undefined) returnValue = true;
|
if(returnValue === undefined) returnValue = true;
|
||||||
|
|
||||||
if(returnValue) {
|
if(returnValue) {
|
||||||
this._debug("Translation successful");
|
if(this.saveQueue.length) {
|
||||||
|
var me = this;
|
||||||
|
this._itemSaver.saveItems(this.saveQueue.slice(),
|
||||||
|
function(returnValue, data) { me.itemsSaved(returnValue, data) });
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this._debug("Translation successful");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if(error) {
|
if(error) {
|
||||||
// report error to console
|
// report error to console
|
||||||
|
@ -1082,6 +1128,7 @@ Zotero.Translate.Base.prototype = {
|
||||||
this._runningAsyncProcesses = 0;
|
this._runningAsyncProcesses = 0;
|
||||||
this._returnValue = undefined;
|
this._returnValue = undefined;
|
||||||
this._aborted = false;
|
this._aborted = false;
|
||||||
|
this.saveQueue = [];
|
||||||
|
|
||||||
Zotero.debug("Translate: Parsing code for "+translator.label, 4);
|
Zotero.debug("Translate: Parsing code for "+translator.label, 4);
|
||||||
|
|
||||||
|
@ -1118,7 +1165,7 @@ Zotero.Translate.Base.prototype = {
|
||||||
"}"+
|
"}"+
|
||||||
"};";
|
"};";
|
||||||
|
|
||||||
if(this instanceof Zotero.Translate.Export) {
|
if(this instanceof Zotero.Translate.Export || this instanceof Zotero.Translate.Import) {
|
||||||
src += "Zotero.Collection = function () {};"+
|
src += "Zotero.Collection = function () {};"+
|
||||||
"Zotero.Collection.prototype.complete = function() { Zotero._collectionDone(this); };";
|
"Zotero.Collection.prototype.complete = function() { Zotero._collectionDone(this); };";
|
||||||
} else if (this instanceof Zotero.Translate.Import) {
|
} else if (this instanceof Zotero.Translate.Import) {
|
||||||
|
@ -1312,7 +1359,7 @@ Zotero.Translate.Web.prototype._getParameters = function() { return [this.docume
|
||||||
Zotero.Translate.Web.prototype._prepareTranslation = function() {
|
Zotero.Translate.Web.prototype._prepareTranslation = function() {
|
||||||
this._itemSaver = new Zotero.Translate.ItemSaver(this._libraryID,
|
this._itemSaver = new Zotero.Translate.ItemSaver(this._libraryID,
|
||||||
Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_DOWNLOAD" : "ATTACHMENT_MODE_IGNORE")], 1);
|
Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_DOWNLOAD" : "ATTACHMENT_MODE_IGNORE")], 1);
|
||||||
this.newItems = this._itemSaver.newItems;
|
this.newItems = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1507,7 +1554,7 @@ Zotero.Translate.Import.prototype._prepareTranslation = function() {
|
||||||
this._progress = undefined;
|
this._progress = undefined;
|
||||||
this._itemSaver = new Zotero.Translate.ItemSaver(this._libraryID,
|
this._itemSaver = new Zotero.Translate.ItemSaver(this._libraryID,
|
||||||
Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_FILE" : "ATTACHMENT_MODE_IGNORE")]);
|
Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_FILE" : "ATTACHMENT_MODE_IGNORE")]);
|
||||||
this.newItems = this._itemSaver.newItems;
|
this.newItems = [];
|
||||||
this.newCollections = this._itemSaver.newCollections;
|
this.newCollections = this._itemSaver.newCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,73 +72,79 @@ Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD = 1;
|
||||||
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE = 2;
|
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE = 2;
|
||||||
|
|
||||||
Zotero.Translate.ItemSaver.prototype = {
|
Zotero.Translate.ItemSaver.prototype = {
|
||||||
"saveItem":function(item) {
|
"saveItems":function(items, callback) {
|
||||||
// if no open transaction, open a transaction and add a timer call to close it
|
// if no open transaction, open a transaction and add a timer call to close it
|
||||||
|
var openedTransaction = false;
|
||||||
if(!Zotero.DB.transactionInProgress()) {
|
if(!Zotero.DB.transactionInProgress()) {
|
||||||
Zotero.debug("Translate: Beginning transaction");
|
|
||||||
Zotero.DB.beginTransaction();
|
Zotero.DB.beginTransaction();
|
||||||
|
openedTransaction = true;
|
||||||
var me = this;
|
|
||||||
Zotero.setTimeout(function() { me._closeTransaction() }, 0);
|
|
||||||
Zotero.showZoteroPaneProgressMeter(Zotero.getString("ingester.scraping"), false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get typeID, defaulting to "webpage"
|
try {
|
||||||
var newItem;
|
var newItems = [];
|
||||||
var type = (item.itemType ? item.itemType : "webpage");
|
for each(var item in items) {
|
||||||
|
// Get typeID, defaulting to "webpage"
|
||||||
if(type == "note") { // handle notes differently
|
var newItem;
|
||||||
newItem = new Zotero.Item('note');
|
var type = (item.itemType ? item.itemType : "webpage");
|
||||||
newItem.libraryID = this._libraryID;
|
|
||||||
newItem.setNote(item.note);
|
|
||||||
var myID = newItem.save();
|
|
||||||
newItem = Zotero.Items.get(myID);
|
|
||||||
} else {
|
|
||||||
if(type == "attachment") { // handle attachments differently
|
|
||||||
newItem = this._saveAttachment(item);
|
|
||||||
if(!newItem) return;
|
|
||||||
var myID = newItem.id;
|
|
||||||
} else {
|
|
||||||
var typeID = Zotero.ItemTypes.getID(type);
|
|
||||||
newItem = new Zotero.Item(typeID);
|
|
||||||
newItem._libraryID = this._libraryID;
|
|
||||||
|
|
||||||
this._saveFields(item, newItem);
|
|
||||||
|
|
||||||
// handle creators
|
if(type == "note") { // handle notes differently
|
||||||
if(item.creators) {
|
newItem = new Zotero.Item('note');
|
||||||
this._saveCreators(item, newItem);
|
newItem.libraryID = this._libraryID;
|
||||||
}
|
newItem.setNote(item.note);
|
||||||
|
var myID = newItem.save();
|
||||||
// save item
|
newItem = Zotero.Items.get(myID);
|
||||||
var myID = newItem.save();
|
} else {
|
||||||
newItem = Zotero.Items.get(myID);
|
if(type == "attachment") { // handle attachments differently
|
||||||
|
newItem = this._saveAttachment(item);
|
||||||
// handle notes
|
if(!newItem) return;
|
||||||
if(item.notes) {
|
var myID = newItem.id;
|
||||||
this._saveNotes(item, myID);
|
} else {
|
||||||
}
|
var typeID = Zotero.ItemTypes.getID(type);
|
||||||
|
newItem = new Zotero.Item(typeID);
|
||||||
// handle attachments
|
newItem._libraryID = this._libraryID;
|
||||||
if(item.attachments) {
|
|
||||||
for(var i=0; i<item.attachments.length; i++) {
|
this._saveFields(item, newItem);
|
||||||
var newAttachment = this._saveAttachment(item.attachments[i], myID);
|
|
||||||
if(newAttachment) this._saveTags(item.attachments[i], newAttachment);
|
// handle creators
|
||||||
|
if(item.creators) {
|
||||||
|
this._saveCreators(item, newItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// save item
|
||||||
|
var myID = newItem.save();
|
||||||
|
newItem = Zotero.Items.get(myID);
|
||||||
|
|
||||||
|
// handle notes
|
||||||
|
if(item.notes) {
|
||||||
|
this._saveNotes(item, myID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle attachments
|
||||||
|
if(item.attachments) {
|
||||||
|
for(var i=0; i<item.attachments.length; i++) {
|
||||||
|
var newAttachment = this._saveAttachment(item.attachments[i], myID);
|
||||||
|
if(newAttachment) this._saveTags(item.attachments[i], newAttachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(item.itemID) this._IDMap[item.itemID] = myID;
|
||||||
|
|
||||||
|
// handle see also
|
||||||
|
this._saveTags(item, newItem);
|
||||||
|
|
||||||
|
// add to new item list
|
||||||
|
newItem = Zotero.Items.get(myID);
|
||||||
|
newItems.push(newItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(openedTransaction) Zotero.DB.commitTransaction();
|
||||||
|
callback(true, newItems);
|
||||||
|
} catch(e) {
|
||||||
|
if(openedTransaction) Zotero.DB.rollbackTransaction();
|
||||||
|
callback(false, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(item.itemID) this._IDMap[item.itemID] = myID;
|
|
||||||
|
|
||||||
// handle see also
|
|
||||||
this._saveTags(item, newItem);
|
|
||||||
|
|
||||||
// add to new item list
|
|
||||||
this.newItems.push(myID);
|
|
||||||
|
|
||||||
newItem = Zotero.Items.get(myID);
|
|
||||||
return newItem;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"saveCollection":function(collection) {
|
"saveCollection":function(collection) {
|
||||||
|
@ -509,15 +515,6 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the transaction when the current code block finishes executing.
|
|
||||||
*/
|
|
||||||
"_closeTransaction":function() {
|
|
||||||
Zotero.debug("Translate: Closing transaction");
|
|
||||||
Zotero.hideZoteroPaneOverlay();
|
|
||||||
Zotero.DB.commitTransaction();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -732,6 +732,24 @@ Zotero.Utilities = {
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.join(delimiter ? delimiter : ", ");
|
return strings.join(delimiter ? delimiter : ", ");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random string of length 'len' (defaults to 8)
|
||||||
|
**/
|
||||||
|
"randomString":function(len, chars) {
|
||||||
|
if (!chars) {
|
||||||
|
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
|
||||||
|
}
|
||||||
|
if (!len) {
|
||||||
|
len = 8;
|
||||||
|
}
|
||||||
|
var randomstring = '';
|
||||||
|
for (var i=0; i<len; i++) {
|
||||||
|
var rnum = Math.floor(Math.random() * chars.length);
|
||||||
|
randomstring += chars.substring(rnum,rnum+1);
|
||||||
|
}
|
||||||
|
return randomstring;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1471,18 +1471,7 @@ if(appInfo.platformVersion[0] >= 2) {
|
||||||
* Generate a random string of length 'len' (defaults to 8)
|
* Generate a random string of length 'len' (defaults to 8)
|
||||||
**/
|
**/
|
||||||
function randomString(len, chars) {
|
function randomString(len, chars) {
|
||||||
if (!chars) {
|
return Zotero.Utilities.randomString(len, chars);
|
||||||
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
|
|
||||||
}
|
|
||||||
if (!len) {
|
|
||||||
len = 8;
|
|
||||||
}
|
|
||||||
var randomstring = '';
|
|
||||||
for (var i=0; i<len; i++) {
|
|
||||||
var rnum = Math.floor(Math.random() * chars.length);
|
|
||||||
randomstring += chars.substring(rnum,rnum+1);
|
|
||||||
}
|
|
||||||
return randomstring;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue