How to log data in DNN admin log or server event log - dotnetnuke

I simply want to debug a controller but I can't watch the variables I get from 2sxc functions.
I tried to log varables via Log4Net writting :
private static readonly ILog Logger = LoggerSource.Instance.GetLogger(typeof(MyClassName));
but the type ILog is not known in a 2sxc controller. Am I missing a reference?
I also found this snippet:
using DotNetNuke.Services.Log.EventLog;
var objEventLog = new EventLogController();
objEventLog.AddLog("Sample Message", "Something Interesting Happened!", PortalSettings, UserId, EventLogController.EventLogType.ADMIN_ALERT)
But I don't know what to send to "PortalSettings" and I don't any clue in the helpers of the 2sxc programming interface.
How do you guys debug 2sxc controllers and log events (not only for debuging)?
Thank you for your help!
Credit of these snippets: Scott McCulloch (https://www.smcculloch.com/code/logging-to-the-dnn-event-log)

This gives part of the answer: http://www.dnnsoftware.com/community-blog/cid/141723/using-log4net-with-dotnetnuke. And, it looks like the namespace is DotNetNuke.Instrumentation.
As for PortalSettings, that's the portal settings for your portal. I think that you'd need to reference DotNetNuke.Entities.Portals, and then use PortalController to retrieve the portal settings object.

Joe Craig's previous post helped me a lot.
So, in a 2sxc application, I now can log in the DNN event log (not the Windows one):
#using DotNetNuke.Services.Log.EventLog;
#using DotNetNuke.Entities.Portals;
#{
var aujourdhui = DateTime.Now;
var objEventLog = new EventLogController();
PortalSettings PortalSettings = new PortalSettings();
objEventLog.AddLog("Debug info", "Variable \"Aujourdhui\" contains: " + aujourdhui.ToString("dddd d MMMM yyyy"), PortalSettings, #Dnn.User.UserID, EventLogController.EventLogType.ADMIN_ALERT);
}
The only little problem is that this PortalSettings returns the first portal even if my 2sxc app runs on the second portal (id=1). I must be missing something. But for now and what I need (debugging), thats Ok for me!

Related

CPQ Quote API, I can't save the quote

I can't save the quote.
Doing the query:
select
ApexClass.name, Id, CreatedDate, CreatedById, JobType,
ApexClassId, Status, JobItemsProcessed, TotalJobItems,
NumberOfErrors, CompletedDate, MethodName, ExtendedStatus,
ParentJobId, LastProcessed, LastProcessedOffset
from
AsyncApexJob
order by
CreatedDate desc
I get this error:
Calculation error on quote Q-13761: "UNAUTHORIZED"
Code:
public with sharing class QuoteCalculator {
public void calculate(QuoteModel quote, String callbackClass) {
system.debug('quote: ' +quote);
system.debug('callbackClass: ' +callbackClass);
QuoteCalculatorContext ctx = new QuoteCalculatorContext(quote, callbackClass);
SBQQ.ServiceRouter.load('SBQQ.QuoteAPI.QuoteCalculator', null, JSON.serialize(ctx));
system.debug('QuoteCalculator.calculate');
}
private class QuoteCalculatorContext {
private QuoteModel quote; //The quote and callbackClass properties are called
in the API code by the exact names seen here.
private String callbackClass; //Altering these property names will cause
calculator API calls to fail.
private QuoteCalculatorContext(QuoteModel quote, String callbackClass) {
this.quote = quote;
this.callbackClass = callbackClass;
}
}
}
anonymous window:
QuoteReader reader = new QuoteReader();
QuoteModel quote = reader.read('a0p1w000BhfXzAAJ');
System.debug(quote);
quote.lineItems[0].record.SBQQ__Quantity__c = 2;
QuoteCalculator calculator = new QuoteCalculator();
calculator.calculate(quote, 'MyCallback')
Preface
I had (almost) the same exact code base as yours, and got the same error message.
In my case there was an other sandbox I could test my code, and it turned out to be working properly there.
Cause
Later found out that the Salesforce CPQ's Calculation Quote API is using Heroku to do the calculations in order to avoid apex limits exhaustion.
From this it can be deducted, that it needs to have a Connected App. I checked the Apps -> Connected Apps setup, and found that no record was listed under the "Connected Apps OAuth Usage" page for the Salesforce CPQ. (On my other sandbox there was a "Steelbrick CPQ" row.)
From this I concluded that this might be the reason for this behaviour.
Seems like something went wrong during the "Authorize new Calculation Service" process. (Or there was a sandbox refresh and something else went wrong during it.)
Solution
The bad news is that the option to authorize a new calculation service is only visible for the first time you configure the package, which you might already done. (Well... if you haven't done, then this is a great news, because your problem is probably solved. :D) (Otherwise read further.)
The good news is I figured out a solution for the case when you already done this, yet that "Steelbrick CPQ" row is missing.
Created a scratch org and installed the Salesforce CPQ package, then before I clicked on the "Authorize new Calculation Service" link under the "Pricing and Calculation" tab in the Settings Editor, I checked the source code in hope of finding something of interest.
I did.
This link: https://rest-na.steelbrick.com/oauth/auth/https%3A%2F%2Ftest.salesforce.com/SBQQ
(⚠️NOTE: You might have to change it according to your location. There are several servers across the globe:
rest-au.steelbrick.com
rest-eu.steelbrick.com
rest-jp.steelbrick.com
rest-na.steelbrick.com
But for me the above pasted link was generated on the settings page. Which is only interesting, because I live in the EU, yet, for some reason I got the link to the rest-NA server... whatever.gif
So just make sure if you click on the link, in the address bar you can find the appropriate salesforce instance URL.)
Conclusion
With this link you won't have to reinstall the package, you just have to click on it, and allow the access from Steelbrick and the missing row will appear, and you will be authorized to use the Calculation API.

Salesforce - Call external API on button click and update custom field from response

So I'll start by saying I'm a C# .Net/Javascript developer with a lot of experience, but I have zero experience with Salesforce. Never ever seen it before today. So, I've been asked by another team to add a custom button to a Contact object, which when clicked calls an external API and updates a custom field in the Contact with the response. It was pitched as "just write some Javascript that calls an API when a button is clicked, it's literally embedded into the page, 15 minute job...".
Following what appears to be quite an outdated document, I've ended up in the Object Manager, selected the Contact object and I'm into the Buttons, Links and Actions page. I'm assuming before this was done using the Execute Javascript behaviour, which in the Lightning version I'm advised against using. So after much Googling I've read about APEX classes, Visualforce Components, Lightning Components, the Salesforce REST API, etc, etc. Not a 15 min job.
Essentially the requirements are to embed a button (or action, or..?) into this Contact page, so that when the sales guy clicks it, it gathers some of the Contact's details and uses them to form an API call to an external service. The data will be used to form a response, which must then be read (as JSON, then parsed) and written into a custom field on the Contact.
What would be the best approach for developing a solution? In the Apex Debug environment I've put together the code to call the API and parse the JSON response, I'm assuming I need to wrap this in an Apex class, with a method that calls this code and returns the response. What I'm not sure of is how to call into this from the button, and update the field in the Contact.
Do I need to do all that from the Apex Class method? passing in a reference to the Contact, or is there another component that needs to sit in between and do this.
Am I right in assuming I'll need to use the Salesforce API to update the Contact?
Any pointers appreciated.
Oh man. It can be a 15 min job but it's definitely "easy when you know how" or have some examples ;)
What's your user interface, Classic or Lightning? Do they have plans to migrate to Lightning soon? I'm assuming it's Lightning if you figured out the "Execute JavaScript" hacks are passé.
Do you care where the button/action will be? Is the top right corner with all other buttons fine or do you want it to be droppable to pretty much any area in the page?
Does the API callout need username, password, maybe certificate? It'll determine whether you need just to whitelist the endpoint on firewall (Setup -> Remote Site Settings) or you'll need something more advanced (Setup -> Named Credentials).
Do you have SFDX command line (CLI), VSCode / are determined to install some tooling? The Lightning Web Components are cutting edge, most sleek etc but you can't create them straight in the browser (at least not yet), you need tooling. Visualforce is OK but nothing special for this use case, Aura components are bit clunky to write - but you can do both in Developer Console without extra tooling.
Parsing the JSON response - depends how complex it is, you can hand-craft parser with JSON.deserializeUntyped() but life's too short for this. Here's nice apex code generator similar to what you'd get from parsing WSDL: https://json2apex.herokuapp.com/
We'll try to do Aura component way. It's ugly, LWC is future but hey, it'll get you started.
Go to Setup -> Remote Site Settings and add new entry with https://en.wikipedia.org/
Create new Apex class:
public with sharing class Stack63364119 {
static final String endpoint = 'https://en.wikipedia.org/w/api.php?action=query&format=json&list=search&srsearch=';
#AuraEnabled
public static String doCallout(Id contactId){
if(contactId == null){
throw new MyException('Missing record id');
}
List<Contact> contacts = [SELECT MailingCountry FROM Contact WHERE Id = :contactId];
if(contacts.isEmpty() || String.isBlank(contacts[0].MailingCountry)){
throw new MyException('Could not find the contact');
}
Contact c = contacts[0];
HttpRequest req = new HttpRequest();
req.setEndpoint(endpoint + c.MailingCountry);
req.setMethod('GET');
HTTPResponse res = new Http().send(req);
System.debug(res.getStatus());
System.debug(res.getBody());
// no special parsing, just chuck it into Description field
// no error handling
if(res.getStatusCode() == 200){
c.Description = res.getBody().abbreviate(32000);
update c;
}
return res.getBody();
}
public class MyException extends Exception{}
}
Make new "Lighning Component" in developer console (it'll be Aura, not LWC). You can tick the last checkbox about "lightning quick action". Name can be same as class but doesn't have to be.
For component (~ html part) paste this
<!-- Loosely based on https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/controllers_server_actions_call.htm -->
<aura:component controller="Stack63364119" implements="force:hasRecordId,force:lightningQuickAction" >
<!-- in the name of all that is holy do not name the JS function same as the Apex class function, it'll give you very cryptic errors to debug -->
<aura:handler name="init" value="{!this}" action="{!c.runCallout}"/>
</aura:component>
For controller (~ JavaScript) paste this
({
runCallout : function(cmp) {
let action = cmp.get('c.doCallout');
action.setParams({contactId : cmp.get('v.recordId')});
action.setCallback(this, function(response){
let state = response.getState();
if (state === "SUCCESS") {
alert('Saved OK: ' + response.getReturnValue());
$A.get("e.force:closeQuickAction").fire(); // if you want to self-close
} else if (state === "ERROR") {
var errors = response.getError();
if (errors) {
if (errors[0] && errors[0].message) {
console.log("Error message: " + errors[0].message);
}
} else {
console.log("Unknown error");
}
}
});
$A.enqueueAction(action);
}
})
Finally go Object Manager -> Contact -> Buttons Links and Actions. Create new Quick Action.
And add it to page layout(s)!
It should get you started. Maybe you'll decide to split it a bit, Apex would only do the callout, return results to UI and if user is happy - updating the contact can be done with one of these: https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/data_service_save_record.htm. Separation of concerns blah blah (but will the user be happy with 2 clicks).
P.S. If you tweak it and it dies but it's hard to see any JavaScript errors - it'll be because default is to run in release mode, SF rewrites your source code a bit, optimises, polyfills for "browsers" like IE11... Go to Setup -> Debug Mode and enable for your user.
P.P.S. In sandbox / dev org it's good idea to go Setup -> Session Settings -> and untick "Enable secure and persistent browser caching to improve performance". Your component will be always fresh, saves some frantic hitting Ctrl+R. Don't do it in prod ;)
I will write to you my approach
Button on the record page/page layout -> lighting component or flow -> Apex class for collecting data -> apex class request & response API(don't forget to add the endpoint to remote site settings)-> parse response and update contact (you can use queries and DML operations inside Apex)
lighting component it will be very simple just have a the apex class as the controller example
<aura:component implements="force:appHostable,lightning:isUrlAddressable,flexipage:availableForAllPageTypes,flexipage:availableForRecordHome,force:hasRecordId,forceCommunity:availableForAllPageTypes,force:lightningQuickAction" controller="contactController" access="global" >
<aura:handler name="init" value="{!this}" action="{!c.fetchContact}"/>
</aura:component>
controller
({
fetchContact : function(component, event, helper) {
helper.fetchContactHelper(component, event, helper);
}
})
helper
({
fetchAccHelper : function(component, event, helper) {
var action = component.get("c.fetchContacts");
action.setParams({
});
action.setCallback(this, function(response){
var state = response.getState();
if (state === "SUCCESS") {
}
});
$A.enqueueAction(action);
}
})
assuming is apex function is fetchContacts and class contactController

Moved my login page, cant login now

I moved my login controls from account/login to the home page and removed the account controller and it's views. It all works fine on my machine (of course) but when I deploy to my test environment I get an error:
No webpage was found for the web address: https://identity.blah.blah/account/login?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fclient_id%3Dclient.js%26redirect_uri%3Dhttp%253A%252F%252Fblah.blah%26respons etc
My front end is Angular 6 and Im using angular-auth-oidc-client.
There's nothing in my code that creates this url. The sts server url is just the root uri, no account/login.
{
"stsServer":"https://identity.blah.blah",
"redirect_url":"http://app.blah.blah",
"client_id":"app.client.js",
"response_type":"id_token token",
"scope":"openid profile api.v1",
"post_logout_redirect_uri":"http://app.blah.blah",
"start_checksession":true,
"silent_renew":true,
"startup_route":"/",
"forbidden_route":"/forbidden",
"unauthorized_route":"/unauthorized",
"log_console_warning_active":true,
"log_console_debug_active":false,
"max_id_token_iat_offset_allowed_in_seconds":"10"
}
Anyone got any ideas? Maybe this is something hard coded somewhere I need to override?
Found it finally. Not the best documentation... :/
var builder = services.AddIdentityServer(SetupIdentityServer)
private static void SetupIdentityServer(IdentityServerOptions
identityServerOptions)
{
identityServerOptions.UserInteraction.LoginUrl = "/";
identityServerOptions.UserInteraction.LogoutUrl = "/Home/Logout";
}

Show Filtered Lookup in CRM2011 from Silverlight Grid

I recently show up all products on a "Lookup" Field on a Inline Silverlight App on the CRM2011 Quote form.
I do this with directly calling the link of the Lookup:
var uri = (ScriptObject)crmUri.Invoke("create", string.Format("/_controls/lookup/lookupinfo.aspx?LookupStyle=single&objecttypes={0}", objectType));
var dArgs = (ScriptObject)HtmlPage.Window.CreateInstance("Object");
dArgs.SetProperty("items", new string[] { "" });
dynamic dlgResult = HtmlPage.Window.Invoke("showModalDialog", uri, dArgs, "dialogWidth:500px;dialogHeight:700px");
Our Customer wants to filter the lookup view on a value of a specific field on the product form.
This field is a optionset and can be 1 or 2.
I tried to add "&$filter=" + "producttypecode/Value" + " eq 1" or "&$filter=" + "producttypecode" + " eq 1" in the link, but this always Returns a error message.
Are there any suggestions?
This is a valid request that I just tested.
ProductSet?$filter=ProductTypeCode/Value eq 1
If that doesn't work I'd recommend the following troubleshooting steps.
Test your full URL in a browser first.
If it works in a browser then fire up fiddler and see what the difference is between the silverlight request and your manual request using a browser.
If you are having difficulty determining the correct full url I'd recommend downloading and becoming familiar with the CRM OData Query Designer. It will allow you to use a GUI to generate your request strings, and test them out. It can be found here.
http://crm2011odatatool.codeplex.com/
We solved this issue by adding a new System View and call it from its URL.

Parse.com iOS SDK: Create New Related Object on Object Save

I'm trying to use cloud code to create a new 'credit' every time a new User is created, the credit is for that user, as in it is a related object. For some reason I can't get writing to the 'Logs' tab to work using lines like console.log(tell me what is going on!); so I'm stumped, and with no way of knowing where I've gone wrong.
Parse.Cloud.afterSave("User", function(request) {
var Credit = Parse.Object.extend("credit");
var credit = new Credit();
credit.set("parent", request.object);
credit.set("expiry", null);
credit.set("type", "Opening");
credit.save();
});
You need to change it to this:
Parse.Cloud.afterSave(Parse.User, function(request)
Parse.User rather than "User".
For whatever reason this doesn't seem to be in the docs here: https://www.parse.com/docs/cloud_code_guide#functions-aftersave

Resources