How to add custom xml tags to sitemap.xml using mvcsitemapprovider? - mvcsitemapprovider

Based on what Google defines in Video sitemaps , There are some xml tags should be added to sitemaps for videos, and it should be like this:
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url>
<loc>http://www.example.com/videos/some_video_landing_page.html</loc>
<video:video>
<video:thumbnail_loc>http://www.example.com/thumbs/123.jpg</video:thumbnail_loc>
<video:title>Grilling steaks for summer</video:title>
<video:description>Alkis shows you how to get perfectly done steaks every
time</video:description>
<video:content_loc>http://www.example.com/video123.mp4</video:content_loc>
<video:player_loc autoplay="ap=1">
http://www.example.com/videoplayer.mp4?video=123</video:player_loc>
<video:duration>600</video:duration>
<video:expiration_date>2009-11-05T19:20:30+08:00</video:expiration_date>
<video:rating>4.2</video:rating>
<video:view_count>12345</video:view_count>
<video:publication_date>2007-11-05T19:20:30+08:00</video:publication_date>
<video:family_friendly>yes</video:family_friendly>
<video:restriction relationship="allow">IE GB US CA</video:restriction>
<video:gallery_loc title="Cooking Videos">http://cooking.example.com</video:gallery_loc>
<video:price currency="EUR">1.99</video:price>
<video:requires_subscription>yes</video:requires_subscription>
<video:uploader info="http://www.example.com/users/grillymcgrillerson">GrillyMcGrillerson
</video:uploader>
<video:live>no</video:live>
</video:video>
I would like to know how is it possible to add these custom tags dynamically to my sitemap.xml?I am using mvcsitemapprovider

well, I could not use the mvcsitemapprovider to achieve my goal yet, but I made this temporary solution and I am trying to make nuget package from it or add it as a feature to the mvcsitemapprovider package, here is the code I added to an ContentResult in a controller and I changed my routeConfig to call this method when the url is look like this "/videoSiteMap.xml" :
public ContentResult VideoSiteMap()
{
XmlDocument xmlDoc = new XmlDocument();
using (XmlWriter writer = xmlDoc.CreateNavigator().AppendChild())
{
//writer.Formatting = Formatting.Indented;
writer.WriteStartDocument();
writer.WriteStartElement("urlset", "http://www.sitemaps.org/schemas/sitemap/0.9");
// add namespaces
writer.WriteAttributeString("xmlns", "video", null, "http://www.google.com/schemas/sitemap-video/1.1");
List<VideoSiteMap> siteMapp = null;
siteMapp = ServiceHelper.GetGoogleSiteMap();//I invoked a service
//you can use a fake loop instead: for (int i = 0; i < 10; i++)
foreach( var content in siteMapp)
{
writer.WriteStartElement("url");
// required
writer.WriteElementString("loc", "http://example.com/myplayer.aspx");
writer.WriteStartElement("video", "video", null);
// start:optional
writer.WriteElementString("video", "thumbnail_loc", null, "http://www.example.com/thumbs/123.jpg");
writer.WriteElementString("video", "title", null, "");
writer.WriteElementString("video", "description", null, "Alkis shows you how to get perfectly done steaks every time");
writer.WriteElementString("video", "content_loc", null, "http://www.example.com/video123.mp4");
writer.WriteStartElement("video", "player_loc", null);
writer.WriteAttributeString("autoplay", "ap=1");
writer.WriteString("http://www.example.com/videoplayer.mp4?video=123");
writer.WriteEndElement(); // video:player_loc
// end:optional
writer.WriteElementString("video", "duration", null, "100");
writer.WriteElementString("video", "expiration_date", null, "2009-11-05T19:20:30+08:00");
writer.WriteElementString("video", "rating", null, "4.2");
writer.WriteElementString("video", "view_count", null, "12345");
writer.WriteElementString("video", "publication_date", null, "2007-11-05T19:20:30+08:00");
writer.WriteElementString("video", "family_friendly", null, "yes");
writer.WriteElementString("video", "category", null, "Cooking");
writer.WriteStartElement("video", "restriction", null);
writer.WriteAttributeString("relationship", "allow");
writer.WriteString("IE GB US CA");
writer.WriteEndElement();
writer.WriteStartElement("video", "gallery_loc", null);
writer.WriteAttributeString("title", "Cooking Videos");
writer.WriteString("http://cooking.example.com");
writer.WriteEndElement();
writer.WriteStartElement("video", "price", null);
writer.WriteAttributeString("currency", "EUR");
writer.WriteString("1.99");
writer.WriteEndElement();
writer.WriteElementString("video", "requires_subscription", null, "yes");
writer.WriteStartElement("video", "uploader", null);
writer.WriteAttributeString("info", "http://www.example.com/users/grillymcgrillerson");
writer.WriteString("GrillyMcGrillerson");
writer.WriteEndElement();
writer.WriteElementString("video", "live", null, "No");
writer.WriteEndElement(); // video:video
writer.WriteEndElement(); //url
}
writer.WriteEndElement(); //urlset
writer.WriteEndDocument();
writer.Close();
}
var stringWriter = new StringWriter();
var xmlTextWriter = XmlWriter.Create(stringWriter);
xmlDoc.WriteTo(xmlTextWriter);
xmlTextWriter.Flush();
return Content(stringWriter.GetStringBuilder().ToString().replace("utf-16","utf-8"), "text/xml", Encoding.UTF8);
}
I replaced utf-16 with utf-8 while returning my xml data because I could not find an easier way to change XmlWriter's result (by default it always returns a utf-16 format)
I hope it help the others, I will be glad if someone help me to make it a nuget package or something :D

since the XML sitemap functionality was not the main purpose of MvcSiteMapProvider but an "extra" feature, it was not made very flexible.
I started working on creating a general purpose way to build (and page) XML sitemaps and support the full spec of Google (including videos). But realized that this should be a completely separate component than MvcSiteMapProvider, and never released it. You are welcome to take what you need from here.

Related

Custom index of Kentico searching in media library is not adding the index to the search

I'm trying to add a custom index for searching but the file is not added to the results.
This is my code for rebuild method, is finding docs in the right media library but those are not being retrieved in results.
try
{
MediaLibraryInfo library = MediaLibraryInfoProvider.GetMediaLibraryInfo("Documents", SiteContext.CurrentSiteName);
if (library != null)
{
var mediaFiles = MediaFileInfoProvider.GetMediaFiles().WhereEquals("FileLibraryID", library.LibraryID);// all files extension
List<string> files = new List<string>();
foreach (MediaFileInfo mediafile in mediaFiles)
{
SearchDocumentParameters documentParameters = new SearchDocumentParameters()
{
Index = srchInfo,
Type = SearchHelper.CUSTOM_SEARCH_INDEX,
Id = Guid.NewGuid().ToString(),
Created = mediafile.FileCreatedWhen
};
ILuceneSearchDocument doc = LuceneSearchDocumentHelper.ToLuceneSearchDocument(SearchHelper.CreateDocument(documentParameters));
doc.AddGeneralField(SearchFieldsConstants.CUSTOM_TITLE, mediafile.FileTitle, true, true);
doc.AddGeneralField("NAME", mediafile.FileName, true, true);
doc.AddGeneralField("DESCRIPTION", mediafile.FileDescription, true, true);
iw.AddDocument(doc);
}
iw.Flush();
iw.Optimize();
}
}
catch (Exception ex)
{
EventLogProvider.LogException("CustomTextFileIndex", "Rebuild", ex);
}
// Always close the index writer
finally
{
iw.Close();
}
The lines for doc.AddGeneralField with custom NAME and DESCRIPTION won't work with Kentico's default naming convention for columns, you should use the following instead:
doc.AddGeneralField(SearchFieldsConstants.CUSTOM_TITLE, mediafile.FileName, true, true);
doc.AddGeneralField(SearchFieldsConstants.CUSTOM_CONTENT, mediafile.FileDescription, true, true);
The default search mechanisms in Kentico won't use any custom column names for their queries. You'd have to create custom Lucene queries for that functionality. If you're already using CUSTOM_TITLE or CUSTOM_CONTENT fields for something else, then simply add the content to that same field, for example:
doc.AddGeneralField(SearchFieldsConstants.CUSTOM_TITLE, $"{mediafile.FileTitle} {mediafile.FileName}", true, true);
You can verify the Lucene index has included the records using a tool called Luke, I use version 3.5 (4+ won't work on the Kentico indexes)
https://code.google.com/archive/p/luke/downloads

Can I delete a file in Acumatica via the API?

I'm creating a file in Acumatica by calling an action from the API, so that I can retrieve the file in my application.
Is it possible to delete the file via API after I'm done with it? I'd rather not have it cluttering up my Acumatica database.
Failing this, is there a recommended cleanup approach for these files?
Found examples of how to delete a file from within Acumatica, as well as how to save a new version of an existing file! The below implementation saves a new version but has the deletion method commented out. Because I built this into my report generation process, I'm not later deleting the report via API, but it would be easy to translate a deletion into an action callable by the API.
private IEnumerable ExportReport(PXAdapter adapter, string reportID, Dictionary<String, String> parameters)
{
//Press save if the SO is not completed
if (Base.Document.Current.Completed == false)
{
Base.Save.Press();
}
PX.SM.FileInfo file = null;
using (Report report = PXReportTools.LoadReport(reportID, null))
{
if (report == null)
{
throw new Exception("Unable to access Acumatica report writer for specified report : " + reportID);
}
PXReportTools.InitReportParameters(report, parameters, PXSettingProvider.Instance.Default);
ReportNode reportNode = ReportProcessor.ProcessReport(report);
IRenderFilter renderFilter = ReportProcessor.GetRenderer(ReportProcessor.FilterPdf);
//Generate the PDF
byte[] data = PX.Reports.Mail.Message.GenerateReport(reportNode, ReportProcessor.FilterPdf).First();
file = new PX.SM.FileInfo(reportNode.ExportFileName + ".pdf", null, data);
//Save the PDF to the SO
UploadFileMaintenance graph = new UploadFileMaintenance();
//Check to see if a file with this name already exists
Guid[] files = PXNoteAttribute.GetFileNotes(Base.Document.Cache, Base.Document.Current);
foreach (Guid fileID in files)
{
FileInfo existingFile = graph.GetFileWithNoData(fileID);
if (existingFile.Name == reportNode.ExportFileName + ".pdf")
{
//If we later decide we want to delete previous versions instead of saving them, this can be changed to
//UploadFileMaintenance.DeleteFile(existingFile.UID);
//But in the meantime, for history purposes, set the UID of the new file to that of the existing file so we can save it as a new version.
file.UID = existingFile.UID;
}
}
//Save the file with the setting to create a new version if one already exists based on the UID
graph.SaveFile(file, FileExistsAction.CreateVersion);
//Save the note attribute so we can find it again.
PXNoteAttribute.AttachFile(Base.Document.Cache, Base.Document.Current, file);
}
//Return the info on the file
return adapter.Get();
}
The response from Acumatica:
S-b (Screen-base) API allows clean way of downloading report generated as file. C-b (Contract-base) simply does not have this feature added. I suggest you provided feedback here: feedback.acumatica.com (EDIT: Done! https://feedback.acumatica.com/ideas/ACU-I-1852)
I think couple of workaround are:
1) use s-b using login from c-b to generate report and get as file (see example below), or
2) create another method to delete the file once required report file is downloaded. For that, you will need to pass back FileID or something to identify for deletion.
example of #1
using (DefaultSoapClient sc = new DefaultSoapClient("DefaultSoap1"))
{
string sharedCookie;
using (new OperationContextScope(sc.InnerChannel))
{
sc.Login("admin", "123", "Company", null, null);
var responseMessageProperty = (HttpResponseMessageProperty)
OperationContext.Current.IncomingMessageProperties[HttpResponseMessageProperty.Name];
sharedCookie = responseMessageProperty.Headers.Get("Set-Cookie");
}
try
{
Screen scr = new Screen(); // add reference to report e.g. http://localhost/Demo2018R2/Soap/SO641010.asmx
scr.CookieContainer = new System.Net.CookieContainer();
scr.CookieContainer.SetCookies(new Uri(scr.Url), sharedCookie);
var schema = scr.GetSchema();
var commands = new Command[]
{
new Value { LinkedCommand = schema.Parameters.OrderType, Value = "SO" },
new Value { LinkedCommand = schema.Parameters.OrderNumber, Value = "SO004425" },
schema.ReportResults.PdfContent
};
var data = scr.Submit(commands);
if(data != null && data.Length > 0)
{
System.IO.File.WriteAllBytes(#"c:\Temp\SalesOrder.pdf",
Convert.FromBase64String(data[0].ReportResults.PdfContent.Value));
}
}
finally
{
sc.Logout();
}
}
Hope this helps. Also, it would be great if you update the stackover post based on these suggestions.
Thanks
Nayan Mansinha
Lead - Developer Support | Acumatica

CDON API RESTful Api GET request

I'm currently working on fetching customer data from cdon, it's an e-commerce platform. They have their API documentation here:
CDON Api Docu
First let me show you my code:
myToken = '<token here>'
myUrl = 'https://admin.marketplace.cdon.com/api/reports/d8578ef8-723d-46cb-bb08-af8c9b5cca4c'
head = {'Authorization': 'token {}'.format(myToken),
'Status':'Online',
'format':'json'}
filters = '?filter={"Status":["Online"],"format": ["json"] }}'
response = requests.get(myUrl + filters, headers=head)
report = response.json()
print(report.products)
This is returning only the parameters. like for example at at this JSON: CDON Github
Status has a value Online this online is a group of itemsthat I only want to get.
What I'm trying to get is a response like this:
{
"Products": [
{
"SKU": "322352",
"Title": "Fabric Cover",
"GTIN": "532523626",
"ManufacturerArticleNumber": "",
"StatusCDON": "Online",
"ExposeStatusCDON": "Buyable",
"InStock": 0,
"InStockCDON": 0,
"CurrentPriceSE": null,
"OrdinaryPriceSE": null,
"CurrentPriceCDONSE": 299.0000,
"OrdinaryPriceCDONSE": null,
"CurrentPriceDK": null,
"OrdinaryPriceDK": null,
"CurrentPriceCDONDK": null,
"OrdinaryPriceCDONDK": null,
"CurrentPriceNO": null,
"OrdinaryPriceNO": null,
"CurrentPriceCDONNO": null,
"OrdinaryPriceCDONNO": null,
"CurrentPriceFI": null,
"OrdinaryPriceFI": null,
"CurrentPriceCDONFI": null,
"OrdinaryPriceCDONFI": null
},
Which means the full list of the items that are Online
How should I put this... among all the API's I tried this one is very confusing, is this even RestFul? If I can achieve the python equivalent of this C# sample code:
public string Post(Guid repordId, string path)
{
var filter = new JavaScriptSerializer().Serialize(new
{
States = new[] { "0" } // Pending state
});
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair("ReportId", repordId.ToString()),
new KeyValuePair("format", "json"),
new KeyValuePair("filter", filter)
});
var httpClient = new HttpClient() { BaseAddress = new Uri("https://admin.marketplace.cdon.com/") };
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("api", ApiKey);
var response = httpClient.PostAsync(path, content).Result;
response.EnsureSuccessStatusCode();
return response.Content.ReadAsStringAsync().Result;
}
I may be able to undestand how this API works, the response that I got was taken manually from their report function in JSON format.
Image
I made many attempts and at that code ( my code ) I stopped, being on this for 4 hours made me give up and ask. Trust that I have searched as many references as I could. It's really confusing.
How do I get the response that I want? Filtering via url? or via header? is this even restful? Help T_T
The documentation states in the first line, emphasis mine:
In order to generate a report you perform a POST call to the reports API with the parameters you wish to use for the report.
Your Python code does not make a POST request, you are trying a GET request. The documentation goes on
[...] to filter on Swedish orders you set the CountryCodes
attribute to “Sweden” and to get returned and cancelled orders you set
the States attribute to 2 and 3. So in the end the filter would look
like this:
{
"CountryCodes": [ "Sweden" ],
"States": ["2", "3"]
}
So you need to prepare a filter object (a dictionary in Python) with the filters you want. Luckily the Python syntax for dictionaries is equivalent (Python is flexible and also allows single-quoted strings):
filter = {
'CountryCodes': [ 'Sweden' ],
'States': [ '0' ]
}
The documentation goes on
You then post the parameters as form data (content-type:
application/x-www-form-urlencoded) so the request body would look like
this:
ReportId=d4ea173d-bfbc-48f5-b121-60f1a5d35a34&format=json&filter={"CountryCodes":["Sweden"],"States":["2","3"]}
application/x-www-form-urlencoded is the default for HTTP post, the requests module knows that and does this for you automatically. All you need to do is to prepare a data dict which will contain the data you want to post.
data = {
'ReportId': 'd4ea173d-bfbc-48f5-b121-60f1a5d35a34',
'format': 'json'
'filter': json.dumps(filter)
}
The filter parameter is supposed to be in JSON format. You must encode that yourself via json.dumps().
import json
head = { ... as above }
filter = { ... as above }
data = { ... as above }
response = requests.post(url, data, header=head)
I'll leave figuring out setting the Authorization header properly as an exercise for you. Partly because it isn't hard, partly because I have no intention of creating an API key with this website just for testing this and partly because it's entirely possible that your current header already works.

contentmd5 azure block/chunk download

So, I'm able to block/chunk upload a large file to Azure Storage via:
// upload blocks
blockBlob.PutBlock(blockId: blockID,
blockData: new MemoryStream(buffer),
contentMD5: blockHash,
accessCondition: null,
options: new BlobRequestOptions()
{
StoreBlobContentMD5 = true,
UseTransactionalMD5 = true
},
operationContext: null);
// set hash for the entire blob
blockBlob.Properties.ContentMD5 = fileHash;
// commit all uploaded blobs
blockBlob.PutBlockList(blockList: blockIDs,
accessCondition: null,
options: new BlobRequestOptions()
{
StoreBlobContentMD5 = true,
UseTransactionalMD5 = true
},
operationContext: null);
Now, I'm trying to download this large file in block/chunk again via:
// download blocks
blockBlob.DownloadRangeToByteArray(target: blobContents,
index: 0,
blobOffset: startPosition,
length: blobContents.Length,
accessCondition: null,
options: new BlobRequestOptions()
{
UseTransactionalMD5 = true
},
operationContext: null);
I found in the blob property MD5 hash of the entire file, not individual block/chunk. I could verify this once all the blocks/chunks are downloaded, i.e.
if (fileHash == blob.Properties.ContentMD5){ /* download was good */ }
To save download time, is there a way to get the saved MD5 hash of each block/chunk so I can compare it before downloading a new block/chunk?
Thoughts?

Kohana URL Confusion

My bootstrap.php file looks like this, and all code is embed in welcome controller->action_index.
Kohana::init(array(
'base_url' => '/kohana/',
'index' => 'index.php'
));
Okay if I put the following in in action_index
form::open('test');
the action is /kohana/index.php/test.
So links appear to be absolute to your root install location, accept when I embed links in action_index index.php!!!
html::anchor('controller');
the href is /kohana/controller not /kohana/index.php/controller.
Now if I put
url::site('controller');
the returned value is /kohana/index.php/controller.
So I figured I would just use
html::anchor(url::site('controller'));
But href is now equal to http://localhost/kohana/kohana/index.php/controller.
What in the world is going on, and how do I fix it?
Kohana url system seems well unintuitive and wrong.
Seems like it is some kind of bug in HTML::anchor implementation.
This happens because of 127th line of html.php (v3.1.2)
$uri = URL::site($uri, $protocol, $index);
In this line $index value is FALSE according to the default anchor function value:
public static function anchor($uri, $title = NULL, array $attributes = NULL, $protocol = NULL, $index = FALSE)
So all you can do now is - to pass manually 5th argument as true like:
html::anchor('controller', null, null, null, true);
or extend Kohana_HTML with custom class like:
class HTML extends Kohana_HTML
{
public static function anchor($uri, $title = NULL, array $attributes = NULL, $protocol = NULL, $index = TRUE)
{
return parent::anchor($uri, $title, $attributes, $protocol, $index);
}
}
or to fill a bug on kohana bugtracker so ko devteam decide what to do.

Resources