Partial Cache Members - silverstripe

I am using the DataObjectsAsPage module. It returns a Datalist ($Items) to the holder page which loops through each $Item. I am also trying to develop a partial caching strategy for the page. I read in the docs that you cannot place cache blocks inside of a loop, so in my DataObjectsAsPageHolder Page, I have the following:
<% cached 'items', LastEdited, CacheSegment %>
<% loop $Items %>
$Me
<% end_loop %>
<% end_cached %>
I checked the silverstripe-cache/cache directory and this seems to be caching the $Items list.
The problem is that I have added a DataExtension to each $Item that allows the admin to set whether or not an $Item is viewable based on the CurrentMember's group. So within each $Me template I have the following:
<% if HasAccess %>
<% end_if %>
I have two issues:
Given the cache key above, if an authorized member is the first to view a page, then the page gets cached and exclusive material gets shown to non-members in subsequent page views.
If I adjust the cache key to the following:
<% cached 'items', Items.max(Created), CacheSegment unless CurrentMember %>
<% loop $Items %>
$Me
<% end_loop %>
<% end_cached %>
Then the content in each $Me template is never cached for members - which is the largest portion of my sites viewers.
Is there a way I can cache the $Items list for members and non-members and still be able to use the HasAccess check on $Item within the loop?

The simplest solution is probably to add the current member's ID to the cache key.
<% cached 'items', LastEdited, CacheSegment, CurrentMember.ID %>
<% loop $Items %>
$Me
<% end_loop %>
<% end_cached %>
However, this will cache the block uniquely for each registered member. To make the caching a little more useful, you should use the current member's groups in the cache key. Unfortunately, there's no easy way that I know of to get a template cache key ready list of groups for a member.
The easiest way to get a round this issue is probably to add a GroupsCacheKey function to your Page class. It's a bit of a dirty solution, but it should work effectively.
// Untested function
public function GroupsCacheKey() {
if ($member = Member::currentUser()) {
return implode('', $member->Groups()->map('ID', 'ID')->toArray());
}
return 'nonmember';
}
Then in your template:
<% cached 'items', LastEdited, CacheSegment, GroupsCacheKey %>
<% loop $Items %>
$Me
<% end_loop %>
<% end_cached %>
There is a better solution than this out there somewhere, but it will work.

Related

Silverstripe: Template behavior of "Up"

In Silverstripe templating, the $Up tag temporarily breaks out of the current loop/with and gives access to the parent scope. While the following template code works, I don't understand why.
$Checked is a list with objects that I loop, in itself containing a list with $Items of which I need to show the first. The object also has an Owner, which I need to access while in the scope of the first item.
I've got the following silverstripe template code:
<% loop $Checked %>
<% with $Items.First %>
<article class="checked-statement">
<span class="statement-outcome h-bg-true-$Verdict.Value">$Verdict.Name</span>
<div class="checked-statement__statement">
<a href="xxx" class="checked-statement__picture"><img
src="$Up.Up.Owner.Photo.CroppedImage(160,160).Url" alt="$Up.Up.Owner.Name.XML" class="h-full-rounded"/></a>
$Up.Up.Owner.Name zei
<h3>
$Up.Up.Title
</h3>
</div>
<a href="yyy" class="checked-statement__argument">
$Up.Up.Statement…
$Top.Icon('chevron-right')
</a>
</article>
<% end_with %>
<% end_loop %>
I would think I would need only one "Up" to get to the scope of $Checked. But I need two, which is strange, as the first Up should break out of the "with", and the second one should break out of the loop, giving me the top level scope, in which the specific item is not available...
Can anyone point me at the fault in my reasoning, or code?
At some point, I believe it was with 3.0 or 3.1, the logic behind $Up changed.
Previously <% with $Items.First %> or back in 2.x <% control $Items.First %> was just one scope level that you could break out of with $Up.
These days, each object/method call/property gets it's own scope. This means:
<% with $Foo.Bar %>Foo's ID: $Up.ID<% end_with %>
<% with $Foo.Bar %>Outside of "with" ID: $Up.Up.ID (== $Top.ID in this case)<% end_with %>
And for deeper nesting you need even more ups:
<% with $Foo.Bar.X %><% loop $Y %>$Up.Up.Up.Up.ID == $Top.ID<% end_loop %><% end_with %>

SilverStripe GroupedList with has_one data

I'm banging my head trying to figure this out and I feel like I'm over thinking it.
So I've built 2 DataObjects (Brand, Product) and 1 Controller/Page (ProductType).
ProductType has many Products
Product has one Brand
Product has one ProductType
Brand has many Products
What I'm looking to do is:
ProductType pages should make available a list of Brands that are used ONLY by the Products of that Type.
Now I have one somewhat working but it feels hacky and not very portable. Example below:
ProductType Controller:
public function getBrands() {
if($products = $this->Products()) {
$group = GroupedList::create($products->sort('BrandID'))->GroupedBy('BrandID');
}
}
Product Type Template:
<% if Brands %>
<ul>
<li>All</li>
<% loop Brands %>
<% loop Children.Limit(1) %>
<li>$Brand.Title</li>
<% end_loop %>
<% end_loop %>
</ul>
<% end_if %>
Is there a way I can build a method in the ProductType controller that would return a DataList of just the Brands used by Products of that type?
Using Silverstripe 3.1.3
Let me know if I need to be more clear about something and thanks!
Ah, i guess you had a misunderstanding of a GroupedList
You already have the Datalist of all products of this product type.
It's the has_many relation
$this->Products()
now you want to group the current products by BrandID. Your method naming might be confusing, so let's rename it a bit:
public function getProductsGroupedByBrands() {
if($products = $this->Products()) {
$group = GroupedList::create($products->sort('BrandID'))
->GroupedBy('BrandID');
}
}
So in your template you have a GroupedList you can loop over.
<% if Products %>
<ul>
<li>All</li>
<% loop ProductsGroupedByBrands %>
<li>
<%-- here you should be able to see the grouping relation --%>
$Brand.Title
<%-- in doubt use the First element to get the current Brand, it's a has_one -->
<% with $Children.First %>
$Brand.Title
<% end_with %>
<ul>
<% loop Children %>
<%-- here are the products grouped by brand --%>
<li>$Title</li>
<% end_loop %>
</ul>
</li>
<% end_loop %>
</ul>
<% end_if %>
See also Silverstripe Docs or Grouping Lists

Using a page variable as an if statements conditon within a control

I have the following problem:
I have the variable $GiftID on my page.
I want to cycle through all of my gift objects using my function getGifts().
When the $ID of the gift is equal to the $GiftID of the page then I want something to happen.
Here is an example of my code:
$GiftID
<% control getGifts %>
<% if CurrentPage.GiftID = ID %>This is it!<% end_if %>
<% end_control %>
Using $CurrentPage.GiftID works when printing inside the control, but how on earth do I access it from within the if statement?
I am using SS 2.9
I have not used ss2.9 yet, but as far as I know you can not do <% if Top.GiftID = ID %> in any 2.x version, you can not compare 2 variables, you can only compare with static vaules. (but it is possible in 3.0)
So you have to do it on php side, if you want to only display the slected gift object, then:
if GiftID is actually the DB field for the has_one relation of Gift then you can just do <% control Gift %> and it will scope the Gift object with the GiftID
If you really have GiftID saved as DB field or otherwise, then can do
public function getGift() { return DataObject::get_by_id('Gift', $this->GiftID); }
both ways you can do <% control Gift %> and it will scope it
If you want to list all gifts and mark the current gift then you need to do it on php side (foreach the set of objects and set a flag on the current object)
You should be able to access the current page with Top:
<% control getGifts %>
<% if Top.GiftID = ID %>This is it!<% end_if %>
<% end_control %>

Silverstripe: Excluding current page from list of the parent's children

Using Silverstripe's "ChildrenOf" syntax, I've been successfully able to list all children of a page's parent. It's being used in a "see also" style list on a page.
I'd like to exclude the current page from the list but unsure how to determine which is the same as the current page, as within the control loop I'm in the parent's scope. Any ideas? Here's a pseudocode of what I'm doing:
<% control ChildrenOf(page-url) %>
<!-- Output some stuff, like the page's $Link and $Title -->
<% end_control %>
there's a built-in page control for this, so to exclude the current page from your list:
<% control ChildrenOf(page-url) %>
<% if LinkOrCurrent = current %>
<!-- exclude me -->
<% else %>
<!-- Output some stuff, like the page's $Link and $Title -->
<% end_if %>
<% end_control %>
see http://doc.silverstripe.org/sapphire/en/reference/built-in-page-controls#linkingmode-linkorcurrent-and-linkorsection
UPDATE
as you mentioned in your comment below that you'd like to use the $Pos control, you need to filter the dataobjectset before iterating over it.
add the following to your Page_Controller class:
function FilteredChildrenOf($pageUrl) {
$children = $this->ChildrenOf($pageUrl);
if($children) {
$filteredChildren = new DataObjectSet();
foreach($children as $child) {
if(!$child->isCurrent()) $filteredChildren->push($child);
}
return $filteredChildren;
}
}
then replace 'ChildrenOf' in your template by 'FilteredChildrenOf':
<% control FilteredChildrenOf(page-url) %>
//use $Pos here
<% end_control
In Silverstripe 3.1 you can use a method like this -
<% loop $Parent.Children %>
<% if $LinkingMode != current %>
<!-- Output some stuff, like the page's $Link and $Title , $Pos etc -->
<% end_if %>
<% end_loop %>
This way you can list all parent's children pages.
See https://docs.silverstripe.org/en/3.1/developer_guides/templates/common_variables/

How can i call class from aspx file?

i have a class which is in App_Code/Kerbooo.cs i want to call that class's method from aspx file (not from code behind) is it possible? if it is, how can i do? thank you very much already now.
If the method is static, then the following should work within the aspx page:
<% Kerbooo.Method1(...) %>
If the method is not static, then you'll need an instance of Kerbooo:
<%
var kerbooo = new Kerbooo();
kerbooo.Method1(...)
%>
First, import the namespace that your code in App_Code uses:
<%# Import Namespace="MyNamespace" %>
If your code isn't in a namespace yet, it's a good idea to put it in one.
Next, you can call your code either with <% code; %> or <%= code %>, depending on whether you want to write the results to the output stream or not.
Data binding, as in <%# %>, requires a little extra work, as do expressions in <%$ %>
You can use <% %> and put your code in between (if you want to write stuff out <%= %> is a short cut for response.write but you need to do this outside of the <% %>
<%
var bob = new Kerbooo();
..do stuff with class
%>
you can mix and match (this does lead to spaghetti code so be carefull)
e.g looping
<table>
<%
var bob = new Kerbooo();
foreach(var thing in bob.GetThings())
{
%>
<tr><td><%=thing.StuffToWrite%><td></tr>
<%}%>
</table>
And your method should be public if your aspx does not inherit from a class in codebehind

Resources