Good evening all,
I'm learning Thymeleaf and web applications in general right now, and for starters, I'm trying to implement a web service with a page where you can view all registered users and filter them.
Since I want some pagination, I have two forms on this page:
a group of buttons linking to the first, previous, next, and last page
a form with various options for filtering, e.g. "username contains" or "min / max age"
My controller looks like this:
#RequestMapping("/users/all")
String showSearchPage(#RequestParam(value="page", required=false, defaultValue = "0") int page,
#CurrentSecurityContext(expression="authentication?.name") String username,
Model model) {
Page<User> userPage = userService.filterUsers(username, "", 0, 100, PageRequest.of(page, 10));
model.addAttribute("userPage", userPage);
model.addAttribute("pageNr", page);
return "users.html";
}
As you can see, I only implemented the buttons yet and always filter for some default values. (The username parameter makes sure that the currently logged in user isn't finding themself in the list.) My button form looks like that:
<form class="button" th:action="#{/users/all}" method="POST">
<button th:disabled="${pageNr == 0}" type="submit"
class="btn btn-primary"
name="page" th:value="0"><<</button>
<button th:disabled="${pageNr == 0}" type="submit"
class="btn btn-primary"
name="page" th:value="${pageNr - 1}"><</button>
<button th:disabled="${pageNr == userPage.getTotalPages - 1}" type="submit"
class="btn btn-primary"
name="page" th:value="${pageNr + 1}">></button>
<button th:disabled="${pageNr == userPage.getTotalPages - 1}" type="submit"
class="btn btn-primary"
name="page" th:value="${userPage.getTotalPages - 1}">>></button>
</form>
So I'm using the request parameter page to only show the requested page.
Now that I'm about to implement the filtering, my first approach would be adding the form to my HTML, and adding some #ModelAttribute FilterForm filterForm to my controller to be able to get the submitted filter values and use them to retrieve the filtered user list. However, when thinking about it, I found the problem that both forms would only submit their own content, and the controller would only get one of both. Therefore, after filtering users, I would inadvertedly revert back to the full user list when changing pages.
What would be the best approach here to make sure that both functions, filtering and pagination, work together properly?
Thanks in advance!
I'd go with HTTP GET method instead of POST. Why so? Reading users is an idempotent and safe operation. The applied filter and page number can be easily bookmarked in that case.
For filters, you can just add more params. Nothing bad about that.
Make it a bit more RESTful. "/users/all" is unnecessary. "/users" should be enough.
#GetMapping("/users")
String showSearchPage(#RequestParam(value="page", required=false, defaultValue = "0") int page,
#RequestParam("minAge") Optional<Integer> minAge,
#RequestParam("maxAge") Optional<Integer> maxAge,
#CurrentSecurityContext(expression="authentication?.name") String username,
Model model) {
// Apply filters too...
Page<User> userPage = userService.filterUsers(username, "", 0, 100, PageRequest.of(page, 10));
model.addAttribute("userPage", userPage);
model.addAttribute("pageNr", page);
model.addAttribute("nextPage", getPageWithFilterUrl(page + 1, minAge, maxAge));
model.addAttribute("previousPage", getPageWithFilterUrl(page - 1, minAge, maxAge));
return "users.html";
}
To preserve filter while moving back and forth:
private String getPageWithFilterUrl(int page, Optional<Integer> minAge, Optional<Integer> maxAge) {
String defaultNextPageUrl = "/users?page=" + page;
String withMinAge = minAge.map(ma -> defaultNextPageUrl + "&minAge=" + ma).orElse(defaultNextPageUrl);
String withMaxAge = maxAge.map(ma -> withMinAge + "&maxAge=" + ma).orElse(withMinAge);
return withMaxAge;
}
I think the answer is not given here yet. I have the same problem now. I am trying in my project with redirectAttributes.addFlashAttribute("searchProductItemDTO", searchProductItemDTO);
But the problem comes when clicking on the Next/Previous buttons - clicking them does not take into account the search criteria.
Here is my total solution:
1) Pay attention here to the JpaSpecificationExecutor<ItemEntity>
#Repository
public interface AllItemsRepository extends
PagingAndSortingRepository<ItemEntity, Long>, JpaSpecificationExecutor<ItemEntity>{
}
2) Pay attention here to the CriteriaBuilder
public class ProductItemSpecification implements Specification<ItemEntity> {
private final SearchProductItemDTO searchProductItemDTO;
private final String type;
public ProductItemSpecification(SearchProductItemDTO searchProductItemDTO, String type) {
this.searchProductItemDTO = searchProductItemDTO;
this.type = type;
}
#Override
public Predicate toPredicate(Root<ItemEntity> root,
CriteriaQuery<?> query,
CriteriaBuilder cb) {
Predicate predicate = cb.conjunction();
predicate.getExpressions().add(cb.equal(root.get("type"), type));
if (searchProductItemDTO.getModel() != null && !searchProductItemDTO.getModel().isBlank()) {
Path<Object> model = root.get("model");
predicate.getExpressions().add(
//!!!!! when we have two relationally connected tables
// cb.and(cb.equal(root.join("model").get("name"), searchProductItemDTO.getModel()));
//when all fields are from the same table ItemEntity:::: the like works case insensitive
cb.and(cb.like(root.get("model").as(String.class), "%" + searchProductItemDTO.getModel() + "%"))
);
}
if (searchProductItemDTO.getMinPrice() != null) {
predicate.getExpressions().add(
cb.and(cb.greaterThanOrEqualTo(root.get("sellingPrice"),
searchProductItemDTO.getMinPrice()))
);
}
if (searchProductItemDTO.getMaxPrice() != null) {
predicate.getExpressions().add(
cb.and(cb.lessThanOrEqualTo(root.get("sellingPrice"),
searchProductItemDTO.getMaxPrice()))
);
}
return predicate;
}
}
3) Pay attention here to the this.allItemsRepository.findAll default usage
//Complicated use
public Page<ComputerViewGeneralModel> getAllComputersPageableAndSearched(
Pageable pageable, SearchProductItemDTO searchProductItemDTO, String type) {
Page<ComputerViewGeneralModel> allComputers = this.allItemsRepository
.findAll(new ProductItemSpecification(searchProductItemDTO, type), pageable)
.map(comp -> this.structMapper
.computerEntityToComputerSalesViewGeneralModel((ComputerEntity) comp));
return allComputers;
}
4) Pay attention here to the redirectAttributes.addFlashAttribute
#Controller
#RequestMapping("/items/all")
public class ViewItemsController {
private final ComputerService computerService;
#GetMapping("/computer")
public String viewAllComputers(Model model,
#Valid SearchProductItemDTO searchProductItemDTO,
#PageableDefault(page = 0,
size = 3,
sort = "sellingPrice",
direction = Sort.Direction.ASC) Pageable pageable,
RedirectAttributes redirectAttributes) {
if (!model.containsAttribute("searchProductItemDTO")) {
model.addAttribute("searchProductItemDTO", searchProductItemDTO);
}
Page<ComputerViewGeneralModel> computers = this.computerService
.getAllComputersPageableAndSearched(pageable, searchProductItemDTO, "computer");
model.addAttribute("computers", computers);
redirectAttributes.addFlashAttribute("searchProductItemDTO", searchProductItemDTO);
return "/viewItems/all-computers";
}
5) Pay attention here to all the search params that we add in the html file, in the 4 sections where pagination navigation
<main>
<div class="container-fluid">
<div class="container">
<h2 class="text-center text-white">Search for offers</h2>
<form
th:method="GET"
th:action="#{/items/all/computer}"
th:object="${searchProductItemDTO}"
class="form-inline"
style="justify-content: center; margin-top: 50px;"
>
<div style="position: relative">
<input
th:field="*{model}"
th:errorclass="is-invalid"
class="form-control mr-sm-2"
style="width: 280px;"
type="search"
placeholder="Model name case Insensitive..."
aria-label="Search"
id="model"
/>
<input
th:field="*{minPrice}"
th:errorclass="is-invalid"
class="form-control mr-sm-2"
style="width: 280px;"
type="search"
placeholder="Min price..."
aria-label="Search"
id="minPrice"
/>
<input
th:field="*{maxPrice}"
th:errorclass="is-invalid"
class="form-control mr-sm-2"
style="width: 280px;"
type="search"
placeholder="Max price..."
aria-label="Search"
id="maxPrice"
/>
</div>
<button class="btn btn-outline-info my-2 my-sm-0" type="submit">Search</button>
</form>
</div>
<h2 class="text-center text-white mt-5 greybg" th:text="#{view_all_computers}">.........All
Computers.......</h2>
<div class="offers row mx-auto d-flex flex-row justify-content-center .row-cols-auto">
<div
th:each="c : ${computers}" th:object="${c}"
class="offer card col-sm-2 col-md-3 col-lg-3 m-2 p-0">
<div class="card-img-top-wrapper" style="height: 20rem">
<img
class="card-img-top"
alt="Computer image"
th:src="*{photoUrl}">
</div>
<div class="card-body pb-1">
<h5 class="card-title"
th:text="' Model: ' + *{model}">
Model name</h5>
</div>
<ul class="offer-details list-group list-group-flush">
<li class="list-group-item">
<div class="card-text"><span th:text="'* ' + *{processor}">Processor</span></div>
<div class="card-text"><span th:text="'* ' + *{videoCard}">Video card</span></div>
<div class="card-text"><span th:text="'* ' + *{ram}">Ram</span></div>
<div class="card-text"><span th:text="'* ' + *{disk}">Disk</span></div>
<div th:if="*{!ssd.isBlank()}" class="card-text"><span th:text="'* ' + *{ssd}">SSD</span></div>
<div th:if="*{!moreInfo.isBlank()}" class="card-text"><span th:text="'* ' + *{moreInfo}">More info</span>
</div>
<div class="card-text"><span th:text="'We sell at: ' + *{sellingPrice} + ' лв'"
style="font-weight: bold">Selling price</span></div>
</li>
</ul>
<div class="card-body">
<div class="row">
<a class="btn btn-link"
th:href="#{/items/all/computer/details/{id} (id=*{itemId})}">Details</a>
<th:block sec:authorize="hasRole('ADMIN') || hasRole('EMPLOYEE_PURCHASES')">
<a class="btn btn-link alert-danger"
th:href="#{/pages/purchases/computers/{id}/edit (id=*{itemId})}">Update</a>
<form th:action="#{/pages/purchases/computers/delete/{id} (id=*{itemId})}"
th:method="delete">
<input type="submit" class="btn btn-link alert-danger" value="Delete"></input>
</form>
</th:block>
</div>
</div>
</div>
</div>
<div class="container-fluid row justify-content-center">
<nav>
<ul class="pagination">
<li class="page-item" th:classappend="${computers.isFirst()} ? 'disabled' : ''">
<a th:unless="${computers.isFirst()}"
class="page-link"
th:href="#{/items/all/computer(size=${computers.getSize()},page=0,model=${searchProductItemDTO.getModel()}, minPrice=${searchProductItemDTO.getMinPrice()},maxPrice=${searchProductItemDTO.getMaxPrice()})}">First</a>
</li>
</ul>
</nav>
<nav>
<ul class="pagination">
<li class="page-item" th:classappend="${computers.hasPrevious() ? '' : 'disabled'}">
<a th:if="${computers.hasPrevious()}"
class="page-link"
th:href="#{/items/all/computer(size=${computers.getSize()},page=${computers.getNumber() - 1},model=${searchProductItemDTO.getModel()}, minPrice=${searchProductItemDTO.getMinPrice()},maxPrice=${searchProductItemDTO.getMaxPrice()})}">Previous</a>
</li>
</ul>
</nav>
<nav>
<ul class="pagination">
<li class="page-item" th:classappend="${computers.hasNext() ? '' : 'disabled'}">
<a th:if="${computers.hasNext()}"
class="page-link"
th:href="#{/items/all/computer(size=${computers.getSize()},page=${computers.getNumber() + 1},model=${searchProductItemDTO.getModel()}, minPrice=${searchProductItemDTO.getMinPrice()},maxPrice=${searchProductItemDTO.getMaxPrice()})}">Next</a>
</li>
</ul>
</nav>
<nav>
<ul class="pagination">
<li class="page-item" th:classappend="${computers.isLast()} ? 'disabled' : ''">
<a th:unless="${computers.isLast()}"
class="page-link"
th:href="#{/items/all/computer(size=${computers.getSize()},page=${computers.getTotalPages()-1},model=${searchProductItemDTO.getModel()},minPrice=${searchProductItemDTO.getMinPrice()},maxPrice=${searchProductItemDTO.getMaxPrice()})}">Last</a>
</li>
</ul>
</nav>`enter code here`
</div>
</div>
</main>
in my app i want to scrape a web page to extract the values i am interesting.
(ShopData is the HtmlNodeCollection)
and my C# code like this :
var ShopName = ShopData.SelectNodes(".//div[#class='shop-name']");
this return null
if i try this, returns the node:
var ShopName1 = ShopData.SelectNodes(".//div[contains(#class, 'shop cf')]")
why .//div[#class='shop-name'] does not work?
if i do ShopData.SelectNodes(".//div[contains(#class, 'shop cf')]").ToList()[0];
then the innertext is empty.
the same time ShopData.SelectNodes(".//div[#class='price']").ToList()[0].InnerText return text normally.
what are the difference between this 2 functions?
my web page looks like this:
<li class="cf card js-product-card">
<div class="shop cf">
<div class="shop-logo js-shop-logo">
<img class="fade-in" src="//a.scdn.gr/ds/shops/logos/2870/mid_20181210114648_f305ba08.jpeg" data-src="//a.scdn.gr/ds/shops/logos/2870/mid_20181210114648_f305ba08.jpeg" alt="Electroholic">
</div>
<i class="icon tooltip-parent js-tooltip-handler trustmark" data-trigger="toggle" data-type="string" data-theme="light" data-content="Το κατάστημα διαθέτει πιστοποίηση GRECA Trustmark που σημαίνει ότι έχει δεσμευτεί να εργαστεί σύμφωνα με τον Eλληνικό και Ευρωπαϊκό (αντίστοιχα) Κώδικα Ηλεκτρονικού Εμπορίου, διασφαλίζοντας δεοντολογικά πρότυπα στην ψηφιακή αγορά.<div>Περισσότερες πληροφορίες στην <a href='http://www.greekecommerce.gr/' target='_blank'>ιστοσελίδα του GRECA.</a></div>" data-placement="left">
<span>GRECA Trustmark</span>
</i>
<div class="shop-name">Electroholic</div>
</div>
<div class="description">
<div class="item">
<h3>
<a title="Πολυμηχάνημα Epson EcoTank ITS L6170 WiFi ink - έως 60 δόσεις" rel="nofollow" class="js-product-link content-placeholder" data-type="title" href="/products/show/32755241">
Πολυμηχάνημα Epson EcoTank ITS L6170 WiFi ink - έως 60 δόσεις</a>
</h3>
<p class="availability"><span class="availability">Παράδοση έως 30 ημέρες</span></p>
</div>
</div>
<div class="price">
<div class="">
<div class="price-content"><a title="Πολυμηχάνημα Epson EcoTank ITS L6170 WiFi ink - έως 60 δόσεις" rel="nofollow" class="js-product-link product-link content-placeholder" data-type="net_price" href="/products/show/32755241">358,00 €</a><span class="extra-cost cf"><em>+ 9,00 €</em> <span>Μεταφορικά</span></span><span class="extra-cost cf"><em>+ 2,00 €</em> <span>Αντικαταβολή</span></span><span class="final-price"><a title="Πολυμηχάνημα Epson EcoTank ITS L6170 WiFi ink - έως 60 δόσεις" rel="nofollow" class="js-product-link content-placeholder" data-type="final_price" href="/products/show/32755241">369,00 €</a></span></div>
</div>
</div>
<div class="shop-details react-expander-bottom js-product-uservoice"><span class="payment-options"><i class="icon tooltip-parent js-tooltip-handler trustmark" data-trigger="toggle" data-type="string" data-theme="light" data-content="Το κατάστημα διαθέτει πιστοποίηση GRECA Trustmark που σημαίνει ότι έχει δεσμευτεί να εργαστεί σύμφωνα με τον Eλληνικό και Ευρωπαϊκό (αντίστοιχα) Κώδικα Ηλεκτρονικού Εμπορίου, διασφαλίζοντας δεοντολογικά πρότυπα στην ψηφιακή αγορά.<div>Περισσότερες πληροφορίες στην <a href='http://www.greekecommerce.gr/' target='_blank'>ιστοσελίδα του GRECA.</a></div>" data-placement="auto vertical"><span>GRECA Trustmark</span></i>
</span>
<div class="shop-expander-tabs">
<button class="shop-tab js-shop-tab icon ">
<div class="rating-with-count react-component">
<a class="rating stars" title="3,9 αστέρια από 1493 χρήστες" href="#reviews">
<div class="rating-wrapper">
<div class="actual-rating blue" itemprop="" style="width: 78%;">1493</div><span itemprop="">3,9</span></div>
</a>
<div class="reviews-count blue">
<a title="1493 αξιολογήσεις χρηστών" href="#reviews">1493</a></div>
</div>
</button>
<button class="shop-tab js-shop-tab icon location-tab multi-shops ">
<span>Περιστέρι, Αττική</span></button>
</div>
<div class="shop-info-object js-shop-info-expander ">
</div>
</div>
</li>
OK, i figure out what is goin on.
the web page use AJAX calls and thats why i cannot see them.
On the bellow code, how can i get the value "Start test"?
<div class="nea-sidebar" _ngcontent-c2="">
<a class="sidebar-item active" href="#/test" _ngcontent-c2="" routerlinkactive="active" ng-reflect-router-link="test" ng-reflect-router-link-active="active">
<i class="sidebar-icon fas fa-play" _ngcontent-c2="" ng-reflect-klass="sidebar-icon fas" ng-reflect-ng-class="fa-play"></i>
<span class="sidebar-label" _ngcontent-c2="">Start Test</span></a>
<a class="sidebar-item" href="#/sequences" _ngcontent-c2="" routerlinkactive="active" ng-reflect-router-link="sequences" ng-reflect-router-link-active="active">
<i class="sidebar-icon fas fa-project-diagram" _ngcontent-c2="" ng-reflect-klass="sidebar-icon fas" ng-reflect-ng-class="fa-project-diagram"></i>
<span class="sidebar-label" _ngcontent-c2="">Sequences</span></a>
.
.
.
/>
I'm using:
element(by.className('sidebar-label')).isDisplayed().then(function(isVisible)
{
if (isVisible) {
// tests -- validar caminho, icon ... label sidebar-label
expect(element(by. className('sidebar-label')).getAttribute('_ngcontent-c2')).toBe('Start Test');
}
But it returns failed due " - Expected '' to be 'Start Test'.
You can do this:
element(by.css('.nea-sidebar '))
.element(by.cssContainingText('.sidebar-label', 'Start Test')).isDisplayed()
.then(function(isDisplayed) {
expect(isDisplayed).toBeTruthy();
});
by.cssContainingText(selector, text) looks for an element with given selector containing a given text. If that element is displayed, that means protractor actually found such element containing such text.
Here i want to Accept button. Here is the HTML.
<div class="friend-request no-pad ng-scope" ng-if="notifications.friendInvites.length > 0">
<p class="rem-head mzero small">
<div class="reminder-lst lst-box ng-scope" ng-repeat="friendInvite in notifications.friendInvites | limitTo:limit">
<span class="img-frame img-circle">
<span class="pull-left rem-detail-a">
<a class="pull-left rem-detail-a pzero" href="friend#/friends/friendprofile/b6c70e4f-bfe1-440d-836c-2e8fdc88540e">
<span class="frndact pull-right">
<a class="ignore" ng-click="ignoreNotification(friendInvite, 'friend')" href="javascript:void(0)">
<a class="accept" ng-click="acceptNotification(friendInvite, 'friend')" href="javascript:void(0)">
<i class="fa fa-lg fa-check-circle green"></i>
</a>
I have tried using below xpath but not working. Can anyone plz help me?
#FindBy(xpath=".//a[ng-click='acceptNotification(friendInvite, 'friend')']/preceding-sibling::i[#css='.fa.fa-lg.fa-check-circle.green']").
Thanks in advance
Assuming that you are looking for the 'A' tag of class accept, you can try
//i[#class="fa fa-lg fa-check-circle green"]/preceding-sibling::a[#class="accept"]
or
//i[#class="fa fa-lg fa-check-circle green"]/preceding-sibling::a[#ng-click="acceptNotification(friendInvite, 'friend')"]
a couple of things:
as TT noted your xpath was missing the # for the attribute selector
the sample you posted is not a well formed xml, expect troubles with xpath if you don't have an xhtml compliant source.
if you use the second example mind to escape either the " or the ' quotes, if you use it inside another expression
I'm trying to read Amazon products into scrapy.
Starting from a random category using this XPath:
products = Selector(response).xpath('//div[#class="s-item-container"]')
for product in products:
item = AmzItem()
item['title'] = product.xpath('//a[#class="s-access-detail-page"]/#title').extract()[0]
item['url'] = product.xpath('//a[#class="s-access-detail-page"]/#href').extract()[0]
yield item
('//div[#class="s-item-container"]') returns all the divs with the products on one category page - that's correct.
Now, how would I get the link to the product?
// stands for where ever in the code
a with the #class should select the right class
But I get a:
item['title'] = product.xpath('//a[#class="s-access-detail-page"]/#title').extract()[0]
exceptions.IndexError: list index out of range
So my list matching this XPath must be empty - but I don't understand why?
EDIT:
The HTML would look like that:
<div class="s-item-container" style="height: 343px;">
<div class="a-row a-spacing-base">
<div class="a-column a-span12 a-text-left">
<div class="a-section a-spacing-none a-inline-block s-position-relative">
<a class="a-link-normal a-text-normal" href="https://rads.stackoverflow.com/amzn/click/com/B0105S434A" rel="nofollow noreferrer"><img alt="Product Details" src="http://ecx.images-amazon.com/images/I/41%2BzrAY74UL._AA160_.jpg" onload="viewCompleteImageLoaded(this, new Date().getTime(), 24, false);" class="s-access-image cfMarker" height="160" width="160"></a>
<div class="a-section a-spacing-none a-text-center">
<div class="a-row a-spacing-top-mini">
<a class="a-size-mini a-link-normal a-text-normal" href="https://rads.stackoverflow.com/amzn/click/com/B0105S434A" rel="nofollow noreferrer">
<div class="a-box">
<div class="a-box-inner a-padding-mini"><span class="a-color-secondary">See more choices</span></div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
<div class="a-row a-spacing-mini">
<div class="a-row a-spacing-none">
<a class="a-link-normal s-access-detail-page a-text-normal" title="Harry Potter Gryffindor School Fancy Robe Cloak Costume And Tie (Size S)" href="https://rads.stackoverflow.com/amzn/click/com/B0105S434A" rel="nofollow noreferrer">
<h2 class="a-size-base a-color-null s-inline s-access-title a-text-normal">Harry Potter Gryffindor School Fancy Robe Cloak Costume And Tie (Size S)</h2>
</a>
</div>
<div class="a-row a-spacing-mini"><span class="a-size-small a-color-secondary">by </span><span class="a-size-small a-color-secondary">Legend</span></div>
</div>
<div class="a-row a-spacing-mini">
<div class="a-row a-spacing-none"><a class="a-size-small a-link-normal a-text-normal" href="http://www.amazon.com/gp/offer-listing/B0105S434A/ref=sr_1_21_olp?s=pet-supplies&ie=UTF8&qid=1435391788&sr=1-21&keywords=pet+supplies&condition=new"><span class="a-size-base a-color-price a-text-bold">$28.99</span><span class="a-letter-space"></span>new<span class="a-letter-space"></span><span class="a-color-secondary">(1 offer)</span><span class="a-letter-space"></span><span class="a-color-secondary a-text-strike"></span></a></div>
</div>
<div class="a-row a-spacing-none"><span name="B0105S434A">
<span class="a-declarative" data-action="a-popover" data-a-popover="{"max-width":"700","closeButton":"false","position":"triggerBottom","url":"/review/widgets/average-customer-review/popover/ref=acr_search__popover?ie=UTF8&asin=B0105S434A&contextId=search&ref=acr_search__popover"}"><i class="a-icon a-icon-star a-star-4"><span class="a-icon-alt">3.9 out of 5 stars</span></i><i class="a-icon a-icon-popover"></i></span></span>
<a class="a-size-small a-link-normal a-text-normal" href="https://rads.stackoverflow.com/amzn/click/com/B0105S434A" rel="nofollow noreferrer">48</a>
</div>
</div>
It should be:
# ------------- The dot makes the query relative to product
product.xpath('.//a[#class="s-access-detail-page"]/#title')
//a[#class="s-access-detail-page"] requires to be exactly class="s-access-detail-page", because xpath works with string but not with meaning :) When you have "multi class ", use contains function
//a[contains(concat(' ', #class, ' '), " s-access-detail-page ")]/#title