First - disclaimer: I know I should not do it like this, but use LazyField instead and that model should not contain logic and I will modify my code accordingly, but I wanted to explore the 1-n relationship between content items and orchard in general.
I'm creating a system where user can respond to selected job offer, so I have two content types - Job, which lists all available jobs, and JobAnswer which contains my custom part and provides link to appropriate Job content item:
public class JobPart : ContentPart<JobPartRecord>
{
public ContentItem Job
{
get
{
if (Record.ContentItemRecord != null)
{
var contentItem = ContentItem.ContentManager.Get(Record.Job.Id);
return contentItem;
}
var nullItem = ContentItem.ContentManager.Query("Job").List().First();
return nullItem;
}
set { Record.Job = value.Record; }
}
}
This works, but I'm not sure how should I handle returning a null contentItem, when creating new content item, now it just returns first Job content item, which is far from ideal.
Related
Developer's version of Acumatica 2020R1 is installed locally. Data for sample tenant MyTenant from training for I-300 were loaded, and WSDL connection established.
DefaultSoapClient is created fine.
However, attempts to export any data by using Getlist cause errors:
using (Default.DefaultSoapClient soapClient =
new Default.DefaultSoapClient())
{
//Sign in to Acumatica ERP
soapClient.Login
(
"Admin",
"*",
"MyTenant",
"Yogifon",
null
);
try
{
//Retrieving the list of customers with contacts
//InitialDataRetrieval.RetrieveListOfCustomers(soapClient);
//Retrieving the list of stock items modified within the past day
// RetrievalOfDelta.ExportStockItems(soapClient);
RetrievalOfDelta.ExportItemClass(soapClient);
}
public static void ExportItemClass(DefaultSoapClient soapClient)
{
Console.WriteLine("Retrieving the list of item classes...");
ItemClass ItemClassToBeFound = new ItemClass
{
ReturnBehavior = ReturnBehavior.All,
};
Entity[] ItemClasses = soapClient.GetList(ItemClassToBeFound);
string lcItemType = "", lcValuationMethod = "";
int lnCustomFieldsCount;
using (StreamWriter file = new StreamWriter("ItemClass.csv"))
{
//Write the values for each item
foreach (ItemClass loItemClass in ItemClasses)
{
file.WriteLine(loItemClass.Note);
}
}
The Acumatica instance was modified by adding a custom field to Stock Items using DAC, and by adding several Attributes to Customer and Stock Items.
Interesting enough, this code used to work until something broke it.
What is wrong here?
Thank you.
Alexander
In the request you have the following line: ReturnBehavior = ReturnBehavior.All
That means that you try to retrieve all linked/detail entities of the object. Unfortunately, some object are not optimized enough to not affect query performance in GetList scenarios.
So, you have to options:
Replace ReturnBehavior=All by explicitly specifying linked/detail entities that you want to retrieve and not include Attributes into the list.
Retrieve StockItem with attributes one by one using Get operation instead of GetList.
P.S. The problem with attributes will most likely be fixed in the next version of API endpoint.
Edit:
Code sample for Get:
public static void ExportItemClass(DefaultSoapClient soapClient)
{
Console.WriteLine("Retrieving the list of item classes...");
ItemClass ItemClassToBeFound = new ItemClass
{
ReturnBehavior = ReturnBehavior.Default //retrieve only default fields (without attributes and other linked/detailed entities)
};
Entity[] ItemClasses = soapClient.GetList(ItemClassToBeFound);
foreach(var entity in ItemClasses)
{
ItemClass itemClass= entity as ItemClass;
ItemClass.ReturnBehavior=ReturnBehavior.All;
// retrieve each ItemClass with all the details/linked entities individually
ItemClass retrievedItemCLass = soapClient.Get(itemClass);
}
In CiviCRM webform, you can 'enable tag and groups'. Configuring those allows you to create option elements in the webform.
This creates one 'widget', one dropdown or set of checkboxes. I have two field instances where I want the user to select a group - say for example
which mailing lists do you want to receive (a,b,c)
what food are you interested in (d,e,f)
a,b,c,d,e and f are all groups. I can not change that.
How could I do that ?
A technical suggestion below, but first, I'd suggest that your real solution is to not use groups for the second question. Groups are set up nicely to handle mailing lists, but if it's to track interests, you'd be better off setting those up as custom fields. It'll solve this immediate issue, and it'll make it easier to deal with tandem searches and so forth (on list b and likes food d).
Now if you must have them as groups, you can create a fake field and move checkboxes into it using jQuery. Create the fake field with one option that you don't care about, but label it "What food are you interested in", or equivalent. Then, edit the Groups field that CiviCRM generated: label it more specifically as "which mailing lists...", and choose Static Options so it doesn't start offering up just any group for someone to choose.
Now, add the following javascript:
// first remove the dummy checkboxes in your fake field
$('#yourdummyfield .form-item').each( function() { $(this).remove(); });
// now move things into the dummy field
$('#yourdummyfield').append( $('#groupsfield .form-item-d');
$('#yourdummyfield').append( $('#groupsfield .form-item-e');
$('#yourdummyfield').append( $('#groupsfield .form-item-f');
From the form processing perspective, they'll all be evaluated as the "groups" field. However, they'll look separate. For better or worse, this will have to be adjusted as you add new groups fields.
After using Andrew Hunts suggestion for a while, I finally solved this on the server side, in a custom module, using webform logic as described here
http://www.webomelette.com/drupal-webform-submission-presave-hook
Basicly, on presave, I look for 2 custom fields containing group ids (mailing and food in the example). Then I add these to the CiviCRM groups field.
I'll add the code below, which has some more logic:
to make it flexible, I use one hidden field to contain the fieldkey
of the civicrm groups selector to add the other fields in. that
field is called 'the_groups_element' (but its not the groups element, it contains the key of the groups element)
there is only one foods group allowed, so before it adds you to a food group, it removes all other food groups from the groups selector.
You could probably make it even more generic, but since I had different logic for the different groups, this was suitable for me.
function getFoodGroups() {
// return foodgroups
}
function getMailGroups() {
// return mailgroups
}
function MYMODULE_webform_submission_presave($node, &$submission) {
$groupselm = '';
$groups_cid = false;
$foods_cid = false;
$mailings_cid = false;
// http://www.webomelette.com/drupal-webform-submission-presave-hook
foreach($node->webform['components'] as $cid=>$comp) {
if ($comp['form_key']=='the_groups_element') {
$groupselm = $comp['value'];
break;
}
}
if ($groupselm) {
foreach($node->webform['components'] as $cid=>$comp) {
if ($comp['form_key']==$groupselm) $groups_cid = $comp['cid'];
if ($comp['form_key']=='the_foods') $foods_cid = $comp['cid'];
if ($comp['form_key']=='the_mailings') $mailings_cid = $comp['cid'];
}
$group_gids = $submission->data[$groups_cid];
if (!$group_gids) $group_gids=array();
if ($foods_cid!==false && $submission->data[$foods_cid]) {
// remove all current foods
foreach ($group_gids as $gidx=>$group_gid) {
foreach (getFoodGroups() as $foodgroup) {
if ($group_gid==$foodgroup['gid']) {
if ($debug) drupal_set_message('removing foodgroup '.$foodgroup['gid']);
unset($group_gids[$gidx]);
}
}
}
// validate and add submitted regions
$foodsgids = $submission->data[$foods_cid];
if (!is_array($foodsgids)) $foodsgids = array($foodsgids);
foreach ($foodsgids as $foodsgid) {
foreach (getFoodGroups() as $foodgroup) {
if ($foodsgid==$foodgroup['gid']) {
$group_gids[]=$foodsgid;
break; // only one food allowed
}
}
}
}
if ($mailings_cid!==false && $submission->data[$mailings_cid]) {
// just add submitted mailings, dont remove any
$mailinggids = $submission->data[$mailings_cid];
if (!is_array($mailinggids)) $mailinggids = array($mailinggids);
foreach ($mailinggids as $mailinggid) {
foreach (getMailGroups() as $mailing) {
if ($mailinggid==$mailing['gid']) {
if ($debug) drupal_set_message('adding mailing '.$mailing['gid']);
$group_gids[]=$mailinggid;
}
}
}
}
$submission->data[$groups_cid] = array_unique($group_gids);
}
I have extended the built-in User ContentType with a Content Picker Field that can be used to select multiple Video ContentItems. This gives me a video multi-picker control on the Edit page of each User.
I love how Orchard CMS makes this so elegantly simple to setup.
Now that I can associate multiple Videos with a User, I'd like to create a Query that will display just the Videos that the currently logged in User has been granted access.
I was hoping to be able to setup a Query using the Projector module, in what I thought was the obvious way (see below), but this returns no results.
This is how I configured the second filter:
Clicked on the + Add a new Filter link on the Edit Query screen
Chose Videos:Ids from the User Content Fields section, like this:
Configured the new filter like this:
What am I doing wrong, or what is the simplest way of diagnosing this issue?
This is how the Content Picker field is defined:
I have spotted my error - it was due to me not having a proper understand of how the filters worked. The Videos:Ids filter in the User Content Fields section does not give access to the current user's list of videos, as I assumed. Instead, it is offering the field to be used in the filter, which would be useful if I were to write a query to produce a list of Users that had access to a specific Video.
It was wishful thinking that it worked the way I wanted, but it's obvious in retrospect how it actually works.
Update: in the hope it's useful for others, here's the custom filter I developed:
public interface IFilterProvider : IEventHandler
{
void Describe(dynamic describe);
}
public class CurrentUserVideosFilter : IFilterProvider
{
private readonly IWorkContextAccessor _workContextAccessor;
public CurrentUserVideosFilter(IWorkContextAccessor workContextAccessor)
{
_workContextAccessor = workContextAccessor;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public void Describe(dynamic describe)
{
describe.For("My Filter Category", T("My Filter Category"), T("My Filter Category"))
.Element("Current User's Videos", T("Current User's Videos"), T("Current User's Videos"),
(Action<dynamic>)ApplyFilter,
(Func<dynamic, LocalizedString>)DisplayFilter,
null
);
}
public void ApplyFilter(dynamic context)
{
var query = (IHqlQuery)context.Query;
context.Query = query.ForType("Video")
.Where(x => x.ContentPartRecord<IdentityPartRecord>(), x => x.InG("Id", GetVideoIdsForCurrentUser()));
}
private IList<int> GetVideoIdsForCurrentUser()
{
var currentUser = _workContextAccessor.GetContext().CurrentUser;
if (currentUser == null) return new int[0];
dynamic item = currentUser.ContentItem;
var videoContentItems = (IEnumerable<ContentItem>)item.User.Videos.ContentItems;
return videoContentItems.Select(i => i.Id).ToList();
}
public LocalizedString DisplayFilter(dynamic context)
{
return T("Videos that have been assigned to the currently logged in user");
}
}
I created this class in a new Orchard module, which contains all my customisations for the site I'm building. Once I installed the module, the filter was immediately available. I assume Orchard uses reflection to seek out all types that implement the IFilterProvider interface.
This is how the filter appears on the Add a Filter screen:
Clicking on the filter shows this screen:
Once the filter has been saved, the query works exactly how I'd like - it shows all videos that have been assigned to the currently logged in user.
Using Grails (or hibernate), I was wanting to know if there is a specific design pattern or method we should be using when implementing a SEARCH of our domain.
For example, on my website, I want to be able to filter(or search) by multiple properties in the domain.
EG: For I have a page which displays a list of HOTELS. When I submit a search form, or if a user clicks "filter by name='blah'", when I enter the controller I get the following:
Domain
String name
String location
Controller
if(params.name && params.reference) {
// Find name/reference
} else if(params.name) {
// Find name
} else if(params.reference) {
// Find reference
} else {
// Find all
}
As you can understand, if there are more properties in the domain to search/filter, the longer the controller gets.
Any help. Please note, I do not want to use the 'searchable' plugin, as this is too complex for my needs.
I would embed these in a named query in the Domain class itself. For example:
Class Hotel {
String name
String city
String country
boolean isNice
static namedQueries = {
customSearch { p ->
if (p?.name) eq('name', p.name)
if (p?.city) eq('name', p.city)
if (p?.country) eq('name', p.country)
if (p?.isNice != null) eq('isNice', p.isNice)
}
}
}
Then later in a controller somewhere ...
def results = Hotel.customSearch(params)
Of course this is a very simple example, but you can expand on it using the same named query or even adding others and chaining them together.
I have a simple IBackgroundTask implementation that performs a query and then either performs an insert or one or more updates depending on whether a specific item exists or not. However, the updates are not persisted, and I don't understand why. New items are created just as expected.
The content item I'm updating has a CommonPart and I've tried authenticating as a valid user. I've also tried flushing the content manager at the end of the Sweep method. What am I missing?
This is my Sweep, slightly edited for brevity:
public void Sweep()
{
// Authenticate as the site's super user
var superUser = _membershipService.GetUser(_orchardServices.WorkContext.CurrentSite.SuperUser);
_authenticationService.SetAuthenticatedUserForRequest(superUser);
// Create a dummy "Person" content item
var item = _contentManager.New("Person");
var person = item.As<PersonPart>();
if (person == null)
{
return;
}
person.ExternalId = Random.Next(1, 10).ToString();
person.FirstName = GenerateFirstName();
person.LastName = GenerateLastName();
// Check if the person already exists
var matchingPersons = _contentManager
.Query<PersonPart, PersonRecord>(VersionOptions.AllVersions)
.Where(record => record.ExternalId == person.ExternalId)
.List().ToArray();
if (!matchingPersons.Any())
{
// Insert new person and quit
_contentManager.Create(item, VersionOptions.Draft);
return;
}
// There are at least one matching person, update it
foreach (var updatedPerson in matchingPersons)
{
updatedPerson.FirstName = person.FirstName;
updatedPerson.LastName = person.LastName;
}
_contentManager.Flush();
}
Try to add _contentManager.Publish(updatedPerson). If you do not want to publish, but just to save, you don't need to do anything more, as changes in Orchard as saved automatically unless the ambient transaction is aborted. The call to Flush is not necessary at all. This is the case both during a regular request and on a background task.