I just started learning Apex recently, and there's still a lot of topics that are hard for me to navigate at this time. I've searched everywhere for a solution that works, but I still haven't been able to figure it out.
I've created a button on my Salesforce org that renders a PDF from a visualforce page, and attaches it to the record as a File. This is to be used with Docusign later on to capture signatures for contracts. The problem is that, when using merge fields in the VF page, they either do not show at all, or I get this exception: "sObject row was retrieved via SOQL without querying the requested field".
Now, the exception explicitly says that I need to query the fields, and this is what I've found I need to do to make this work, but I have not been able to figure out how to do this properly. I've tried running a query in several places in my controller extension to no avail (I am using a standardController that SF created for my custom object).
Here's my extension's code:
public class attachPDFToQuote {
public final i360__Quote__c q {get; set;} //Quote object
//constructor
public attachPDFToQuote (ApexPages.StandardController stdController) {
q = (i360__Quote__c)stdController.getRecord();
/* for(i360__Quote__c query:[SELECT Id, Correspondence_Name__c, Name FROM i360__Quote__c WHERE Id=: q.Id]){
System.debug(i360__Quote__c.Correspondence_Name__c);
}*/
}
public PageReference attachPDF() {
/* for(i360__Quote__c query:[SELECT Id, Correspondence_Name__c, Name FROM i360__Quote__c WHERE Id=: q.Id]){
System.debug(i360__Quote__c.Correspondence_Name__c);
}*/
//generate and attach the PDF document
PageReference pdfPage = Page.ProjectAgreement;
Blob pdfBlob; //create a blob for the PDF content
if (!Test.isRunningTest()) { //if we are not in testing context
pdfBlob = pdfPage.getContent(); //generate the pdf blob
} else { //otherwise, we are in testing context. Create the blob manually.
pdfBlob = Blob.valueOf('PDF');
}
ContentVersion cvAttach = new ContentVersion(ContentLocation= 'S');
cvAttach.PathOnClient= 'Project Agreement.pdf';
cvAttach.Title= 'Project Agreement';
cvAttach.VersionData= pdfBlob;
insert cvAttach;
Id conDoc = [SELECT ContentDocumentID FROM ContentVersion WHERE Id=: cvAttach.Id].ContentDocumentId;
ContentDocumentLink ConDocLink = new COntentDocumentLink();
conDocLink.LinkedEntityId= q.Id;
conDocLink.ContentDocumentId= conDoc;
conDocLink.ShareType= 'V';
insert conDocLink;
//redirect the user
PageReference pageWhereWeWantToGo = new ApexPages.StandardController(q).view(); //redirect the User back to the Quote detail page
pageWhereWeWantToGo.setRedirect(true); //indicate that the redirect should be performed on the client side
return pageWhereWeWantToGo; //send the User on their way
}
}
I kept the commented code where I try to query the object fields so they show in VF. I also tried a couple of different ways, but nothing seems to work. Please let me know if I need to add anything else.
Thank you!
You didn't post your Visualforce page's code.
Even if it's same page (if your apex class is used in ProjectAgreement VF as <apex:page standardController="i360__Quote__c" extensions="attachPDFToQuote" - the act of grabbing a PDF version of the page counts as callout, a separate http traffic to fresh instance of the page so to speak.
So I suspect you need something like
PageReference pdfPage = Page.ProjectAgreement;
pdfPage.getParameters().put('id', q.Id);
Blob = pdfPage.getContent();
If that works... next step would be to look at your VF code.
If the page has merge fields such as {!i360__Quote__c.Name}, {!i360__Quote__c.Correspondence_Name__c} then magic should happen. Salesforce should figure out which fields are needed by looking at your VF page and silently query them for you. So you wouldn't even need the query in your constructor, you could just save stdController.getId() to class variable and then use that id in pdfPage.getParameters().set(...)
But if your VF page has references to {!quote.Correspondence_Name__c} then you need to keep the explicit query in there.
Related
I need to send an email notification to the record owner and manager once the opportunity is closed-won.
adding only owner email works fine
adding only manager email works fine
But if I add both together with coma, {!$Record.Owner.Email},{!$Record.Engagement_Manager__r.Email} I'm getting error.
what is the correct way to add it?
You can try creating a Formula Resource in your flow like this but, in your case, using $Record.Owner.Email and $Record.Engagement_Manager__r.Email:
Then, you can use this Resource in your Email Action:
Try the below code and let me know if it works.
global class SendPurchaseOrderEmail {
WebService static void sendEmail(String poId) {
List<Messaging.SingleEmailMessage> emails = new List<Messaging.SingleEmailMessage>();
String theTemplate = [SELECT Id FROM EmailTemplate WHERE DeveloperName = 'Purchase_Order_With_Items'].Id;
User theUser = [SELECT Id FROM User WHERE Name = 'user name goes here'];
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setSaveAsActivity(false);
mail.setTemplateId(theTemplate);
mail.setWhatId(poId);
mail.setTargetObjectId(theUser.Id);
mail.setToAddresses(new String[] { 'TestUser#salesforce.com' ,'abc#test.com'}); //add other emails here.
emails.add(mail);
Messaging.sendEmail(emails);
}
}
Please refer below link for more details.
https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_sendemail.htm
This is because you have to pass a direct email address there.
Instead of that, you can create a collection variable, store the emails into it, then pass that variable to email addresses (collection) field.
Note: you can only store upto 5 emails into that colllection variable at a time.
Hi For that you can simply add collection Variable.
For that variable assign multiple values to it. So that you can send email to both record owner as well as manager.
From New Resource Select the Variable and click Allow Multiple Values and Data-type as text.
Then by using Assignment. Add the following email Address to it Please refer the below image.
I hope you have got the solution
Thanks
We have a web application that users log into and consume our products. From this application, we'd like to have a form that users can submit to create cases in our Salesforce instance. I'm looking for a REST API endpoint that I can POST the new case information to, which will then create a new case record in Salesforce. I'm a little confused on the right way to approach this based on the Salesforce docs (Apex, Lightning Platform, Force.com, etc.). Has anyone implemented this or can share the right approach?
Easiest way to would be to create a force.com site, which is essentially a visualforce page. Your page can then use a controller to read values and create Cases.
For e.g. this visualforce page updates a custom object record by using id passed in url:
<apex:page controller="MyService"></apex:page>
#RestResource(urlMapping='/myservice')
global class MyService {
#HttpGet
global static void doGet() {
RestContext.response.addHeader('Content-Type', 'text/plain');
String id = RestContext.request.params.get('id');
abc__c veh = [select name, abc__c from abc__c where id =:id];
if(veh!=null)
{
veh.abc__c = true;
try {
update veh;
} catch (DMLException e) {
RestContext.response.responseBody = Blob.valueOf('DML ERROR');
}
RestContext.response.responseBody = Blob.valueOf('OK');
}
else
RestContext.response.responseBody = Blob.valueOf('FAIL');
}
}
I am building a REST API for a DotNetNuke 6 website, making use of DNN's MVC-based Services Framework. However, I don't have any background in authentication, so I'm not even sure where to start.
Basically, we want our clients to be able to make GET requests for their portal's data, and we want some clients (but not all) to be able to POST simple updates to their user data.
I've been trying to search for information, but the trouble is I'm not sure what I'm searching for. DNN has different logins and roles, but I'm not sure if or how they factor in. I've heard of things like oAuth but my understanding of it is at the most basic level. I don't know if it's what I need or not and if or how it applies to DNN. Can anyone point me in the right direction?
UPDATE:
Based on the answer below about tying it with a module and further research, here is what I have done:
I created a module just for this service, and I added two special permissions for it: "APIGET" and "APIPOST." I assigned these to some test roles/test accounts in DNN. I wrote a custom authorize attribute that, given the module ID, checks if the current user has the necessary permission (either through roles or directly). As far as I can tell, tab ID is irrelevant in my case.
It appears to be working both with a web browser (based on the DNN account I'm logged into) and with a php script that sends an HTTP request with an account username/password.
The authorize attribute:
using DotNetNuke.Entities.Modules;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Security;
using DotNetNuke.Security.Permissions;
using System.Web;
public class MyAuthorize : DotNetNuke.Web.Services.AuthorizeAttributeBase
{
public const string AuthModuleFriendlyName = "MyAuthModule";
public const string GETPermission = "APIGET";
public const string POSTPermission = "APIPOST";
public string Permission { get; set; }
protected override bool AuthorizeCore(HttpContextBase context)
{
ModuleController mc = new ModuleController();
ModuleInfo mi = mc.GetModuleByDefinition(PortalController.GetCurrentPortalSettings().PortalId, AuthModuleFriendlyName);
ModulePermissionCollection permCollection = mi.ModulePermissions;
return ModulePermissionController.HasModulePermission(permCollection, Permission);
}
}
The controller:
("mytest" is the endpoint for both GET and POST)
public class MyController : DnnController
{
[ActionName("mytest")]
[AcceptVerbs(HttpVerbs.Get)]
[DnnAuthorize(AllowAnonymous = true)]
[MyAuthorize(Permission = MyAuthorize.GETPermission)]
public string myget(string id = "")
{
return "You have my permission to GET";
}
[ActionName("mytest")]
[AcceptVerbs(HttpVerbs.Post)]
[DnnAuthorize(AllowAnonymous = true)]
[MyAuthorize(Permission = MyAuthorize.POSTPermission)]
public string mypost(string id = "")
{
return "You have my permission to POST";
}
}
The main way that you tie a service in the DNN Services Framework into DNN permissions is to associate the permissions with a module instance. That is, you'll require users of your service to identify which module they're calling from/about (by sending ModuleId and TabId in the request [headers, query-string, cookies, form]), then you can indicate what permissions they need on that module to take a particular action on the service.
You can use the SupportedModules attribute on your service, and pass in a comma-delimited list of module names, to ensure that only your own modules are being allowed. Then, add the DnnModuleAuthorize attribute at the service or individual action level to say what permission the user needs on that module. In your instance, you can also add the AllowAnonymous attribute on the GET actions, and have one DnnModuleAuthorize on the service, for the POST methods (and anything else). Note that you cannot put the AllowAnonymous attribute on the controller; it will override authorizations put at the action, making it impossible to make actions more restrictive.
You'll also want to add the ValidateAntiForgeryToken attribute on the POST actions, to protect against CSRF attacks.
If you don't have a module that naturally associates its permissions with your service, you can create one just for that purpose, solely to expose itself as a permissions management utility.
Once you've figured out the authorization piece above, DNN will take care of authentication using your forms cookie (i.e. AJAX scenarios are taken care of automatically), or via basic or digest authentication (for non-AJAX scenarios). That said, if you're doing non-AJAX, you'll need to figure out a way to validate the anti-forgery token only when it applies.
The Services Framework in DNN is what you are after. It allows you to provide a REST API that plugs directly into DNN security.
Here are some articles to help you get started:
http://www.dotnetnuke.com/Resources/Wiki/Page/Services-Framework-WebAPI.aspx
http://www.dotnetnuke.com/Resources/Blogs/EntryId/3327/Getting-Started-with-DotNetNuke-Services-Framework.aspx
Note, there are some difference in DNN 6 and DNN 7 when using the Services Framework:
http://www.dotnetnuke.com/Resources/Blogs/EntryId/3514/Converting-Services-Framework-MVC-to-WebAPI.aspx
Just wanted to note that the DnnModuleAuthorize attribute takes a PermissionKey parameter for custom permissions so you can do stuff like this:
[DnnModuleAuthorize(PermissionKey = "DELETEDATA")]
[HttpPost]
public HttpResponseMessage DeleteData(FormDataCollection data)
It doesn't look like you can supply your own error message with this so you might to wrap your method body like this instead and leave off the custom permission attribute:
[DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.View)]
[HttpPost]
public HttpResponseMessage DeleteData(FormDataCollection data)
{
var errorMessage = "Could not delete data";
if (ModulePermissionController.HasModulePermission(ActiveModule.ModulePermissions,"DELETEDATA"))
{
// do stuff here
}
else
{
errorMessage = "User does not have delete permission";
}
var error = new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content =
new StringContent(
errorMessage)
};
return error;
}
Just wanted to add to #Richards comment for using the [DnnModuleAuthorize(PermissionKey = "DELETEDATA")] for custom permissions.
The full attribute should be:
[DnnModuleAuthorize(PermissionKey = "DELETEDATA", AccessLevel = SecurityAccessLevel.Edit)]
Leaving it blank does nothing as shown here: https://github.com/dnnsoftware/Dnn.Platform/blob/f4a5924c7cc8226cfe79bbc92357ec1a32165ada/DNN%20Platform/Library/Security/Permissions/PermissionProvider.cs#L810
I guess you require a plugin that allows you to construct GET and POST APIs. you can use this plugin I found on the DNN store. https://store.dnnsoftware.com/dnn-rest-api-custom-api-authentication-authorization.
I have setup a CRUD area on my frontendAPI.php file (testing my models)... and I even managed to secure it. I would like to do this the proper way... I would like to establish a separate directory/ Page for the Admins. Please advise on this.
Still new at this but I'm trying to do the same for a news page, think i've got the login part working but having problems with the CRUD (will post a question on it shortly) - i have a table to populate with data from an rss feed (but will be manually populated with a CRUD to start with) and then have a page on the front end to pull out the details using views to format each news story.
Create a new directory called /page/Admin
Create a new file here based on the function e.g. news.php containing
class page_admin_news extends Page {
function init(){
parent::init();
$p=$this;
$crud=$p->add('CRUD');
$g=$crud->setModel('News');
if($crud->grid)
$crud->grid->addPaginator(30);
}
}
In Frontend.php, you need to enable the login - for an admin only access, the BasicAuth may be sufficient but there are also classes to use a database to obtain username and password infromation e.g. for a membership site - heres the basic one.
// If you wish to restrict access to your pages, use BasicAuth class
$auth=$this->add('BasicAuth')
->allow('demo','demo')
;
You need to modify Frontend.php to enable pages that can be viewed
without being logged in
$auth->allowPage('index');
$auth->allowPage('news');
$auth->allowPage('links');
$auth->allowPage('About');
if (!$auth->isPageAllowed($this->api->page))
{
$auth->check();
}
And also in Frontend.php, you need to create a different menu if logged in. Note the login and logout pages dont actually exist.
if ($auth->isLoggedIn())
{
$this->add('Menu',null,'Menu')
->addMenuitem('News','admin_news')
->addMenuitem('logout')
;
} else {
$this->add('Menu',null,'Menu')
->addMenuitem('News','news')
->addMenuitem('Links','links')
->addMenuItem('About')
->addMenuItem('Login')
;
}
When you login, it goes to page/index.php by default so if you want it to redirect to a particular page when you log in so you can add this to page/index.php
class page_index extends Page {
function init(){
parent::init();
$p=$this;
if($this->api->auth->isLoggedIn())
$this->api->redirect('admin_news');
Hope that helps.
Trev
I am working on a web app project that has been in development for long time. The app has two sides, the majority of the site is publicly accessible. However, there are sections that require the user to be logged in before they can access certain content.
When the user logs in they get a sessionid (GUID) which is stored in a table in the database which tracks all sort for data about the user and their activity.
Every page of the app was written to look if this session id variable exists or not in the querystring. If a user tries to access one of these protected areas, the app checks to see if this sessiond variable is in the querystring. If i is not, they are redirected to the login screen.
The flow of the site moves has the user moving seamlessly from secured areas to non-secured areas, back and forth, etc.
So we did a test run with the Google Custom Search and it does an awesome job picking up all our dynamic content in these public areas. However, we have not been able to figure out how to pass the sessionid along with the search results IF the user is logged in already.
Is it possible to pas querystring variables that already exist in the url along with the search results?
As far as I know, this is not possible. Google doesn't give you the possibilty to modify the URL's of the Search Results in their Custom Search.
A possible solution would be to store your Session-Key to a Cookie, rather than passing it with every URL.
Use the parseQueryFromUrl function
function parseQueryFromUrl () {
var queryParamName = "q";
var search = window.location.search.substr(1);
var parts = search.split('&');
for (var i = 0; i < parts.length; i++) {
var keyvaluepair = parts[i].split('=');
if (decodeURIComponent(keyvaluepair[0]) == queryParamName) {
return decodeURIComponent(keyvaluepair[1].replace(/\+/g, ' '));
}
}
return '';
}
Select RESULTS ONLY option in the Look & Feel and it will provide you with the code.
www.google.com/cse/