Skip to content

Commit 3404f1c

Browse files
committed
feat(spp_cel_registry_search): add pagination and access control
- Add pagination (50 per page) with prev/next controls in top-right - Pass offset parameter through cel_service to cel_executor for server-side pagination via search(limit, offset) - Use phone_number_ids (spp.phone.number) instead of phone char field - Add check_search_access() server-side method and JS onWillStart guard to restrict direct URL access to CEL Search User group
1 parent 51b8bee commit 3404f1c

4 files changed

Lines changed: 120 additions & 16 deletions

File tree

spp_cel_domain/models/cel_executor.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ def compile_and_preview(
392392
model: str,
393393
expr: str,
394394
limit: int = 50,
395+
offset: int = 0,
395396
fields: list[str] | None = None,
396397
materialize_sql: bool = False,
397398
) -> dict[str, Any]:
@@ -465,15 +466,22 @@ def compile_and_preview(
465466
preview_ids = [] # Count-only mode
466467
elif limit == 0:
467468
# Use the default from the method signature (50)
468-
preview_recordset = self.env[model].search(final_domain, limit=50)
469+
preview_recordset = self.env[model].search(final_domain, limit=50, offset=offset)
469470
preview_ids = preview_recordset.ids
470471
else:
471-
preview_recordset = self.env[model].search(final_domain, limit=limit)
472+
preview_recordset = self.env[model].search(final_domain, limit=limit, offset=offset)
472473
preview_ids = preview_recordset.ids
473474

474475
# Read full record data if fields requested (for JSON-safe preview)
475476
if preview_ids and fields:
476-
preview_data = preview_recordset.read(fields)
477+
# Replace phone_number_ids with phone for reading, then enrich
478+
read_fields = [f for f in fields if f != "phone_number_ids"]
479+
preview_data = preview_recordset.read(read_fields)
480+
if "phone_number_ids" in fields:
481+
for rec_data in preview_data:
482+
partner = preview_recordset.filtered(lambda r: r.id == rec_data["id"])
483+
phones = partner.phone_number_ids.filtered(lambda p: not p.disabled).mapped("phone_no")
484+
rec_data["phone_numbers"] = phones
477485
# Enrich explanation with metrics info if any
478486
metrics_section = ""
479487
if metrics_info:

spp_cel_domain/models/cel_service.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,19 @@ class CELService(models.AbstractModel):
3232
_name = "spp.cel.service"
3333
_description = "CEL Expression Service"
3434

35+
@api.model
36+
def check_search_access(self):
37+
"""Check if the current user has CEL search access."""
38+
return self.env.user.has_group("spp_cel_registry_search.group_cel_search_user")
39+
3540
@api.model
3641
def compile_expression(
3742
self,
3843
expression,
3944
profile,
4045
base_domain=None,
4146
limit=0,
47+
offset=0,
4248
fields=None,
4349
materialize_sql=False,
4450
):
@@ -109,6 +115,7 @@ def compile_expression(
109115
root_model,
110116
expanded_expression,
111117
limit=limit,
118+
offset=offset,
112119
fields=fields,
113120
materialize_sql=materialize_sql,
114121
)

spp_cel_registry_search/static/src/js/cel_search_portal.js

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* - Profile selection (Individuals/Groups)
1414
*/
1515

16-
import {Component, useState} from "@odoo/owl";
16+
import {Component, onWillStart, useState} from "@odoo/owl";
1717
import {registry} from "@web/core/registry";
1818
import {useService} from "@web/core/utils/hooks";
1919
import {_t} from "@web/core/l10n/translation";
@@ -36,6 +36,8 @@ export class CelSearchPortal extends Component {
3636

3737
this.editorApi = null;
3838

39+
onWillStart(() => this._checkAccess());
40+
3941
this.state = useState({
4042
profile: "registry_individuals",
4143
celExpression: "",
@@ -45,9 +47,25 @@ export class CelSearchPortal extends Component {
4547
totalCount: 0,
4648
hasMoreResults: false,
4749
validation: null,
50+
currentPage: 1,
4851
});
4952
}
5053

54+
async _checkAccess() {
55+
const allowed = await this.orm.call(
56+
"spp.cel.service",
57+
"check_search_access",
58+
[]
59+
);
60+
if (!allowed) {
61+
this.notification.add(
62+
_t("You do not have permission to access Advanced Search."),
63+
{type: "danger"}
64+
);
65+
this.action.doAction("mail.action_discuss", {clearBreadcrumbs: true});
66+
}
67+
}
68+
5169
onEditorReady(api) {
5270
this.editorApi = api;
5371
}
@@ -69,7 +87,7 @@ export class CelSearchPortal extends Component {
6987
this.state.validation = result;
7088
}
7189

72-
async performSearch() {
90+
async performSearch(page = 1) {
7391
const expression = this.state.celExpression.trim();
7492

7593
if (!expression) {
@@ -92,6 +110,8 @@ export class CelSearchPortal extends Component {
92110
this.state.isSearching = true;
93111
this.state.hasSearched = true;
94112

113+
const offset = (page - 1) * SEARCH_RESULT_LIMIT;
114+
95115
try {
96116
// Request preview records directly from backend to avoid JSON serialization
97117
// issues with SQL subquery domains
@@ -103,11 +123,12 @@ export class CelSearchPortal extends Component {
103123
expression: expression,
104124
profile: this.state.profile,
105125
limit: SEARCH_RESULT_LIMIT,
126+
offset: offset,
106127
fields: [
107128
"id",
108129
"name",
109130
"is_group",
110-
"phone",
131+
"phone_number_ids",
111132
"email",
112133
"registration_date",
113134
"disabled",
@@ -128,7 +149,8 @@ export class CelSearchPortal extends Component {
128149

129150
this.state.results = records;
130151
this.state.totalCount = count;
131-
this.state.hasMoreResults = count > SEARCH_RESULT_LIMIT;
152+
this.state.currentPage = page;
153+
this.state.hasMoreResults = count > offset + records.length;
132154
} catch (error) {
133155
console.error("[CelSearchPortal] Search error:", error);
134156
this.notification.add(
@@ -145,13 +167,33 @@ export class CelSearchPortal extends Component {
145167
}
146168
}
147169

170+
goToPage(page) {
171+
this.performSearch(page);
172+
}
173+
174+
get totalPages() {
175+
return Math.ceil(this.state.totalCount / SEARCH_RESULT_LIMIT);
176+
}
177+
178+
get showingFrom() {
179+
return (this.state.currentPage - 1) * SEARCH_RESULT_LIMIT + 1;
180+
}
181+
182+
get showingTo() {
183+
return Math.min(
184+
this.state.currentPage * SEARCH_RESULT_LIMIT,
185+
this.state.totalCount
186+
);
187+
}
188+
148189
clearSearch() {
149190
this.state.celExpression = "";
150191
this.state.hasSearched = false;
151192
this.state.results = [];
152193
this.state.totalCount = 0;
153194
this.state.hasMoreResults = false;
154195
this.state.validation = null;
196+
this.state.currentPage = 1;
155197

156198
if (this.editorApi) {
157199
this.editorApi.clear();

spp_cel_registry_search/static/src/xml/cel_search_portal.xml

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
<button
6666
class="btn btn-primary"
6767
type="button"
68-
t-on-click="performSearch"
68+
t-on-click="() => this.performSearch()"
6969
t-att-disabled="!canSearch || state.isSearching"
7070
>
7171
<t t-if="state.isSearching">
@@ -108,15 +108,58 @@
108108
class="d-flex justify-content-between align-items-center mb-3"
109109
>
110110
<h5 class="mb-0" aria-live="polite">
111-
<t t-if="state.hasMoreResults">
112-
Showing <t t-esc="state.results.length" /> of <t
113-
t-esc="state.totalCount"
114-
/> Result(s)
111+
<t t-if="state.totalCount > 0">
112+
Showing <t t-esc="showingFrom" /><t
113+
t-esc="showingTo"
114+
/> of <t t-esc="state.totalCount" /> Result(s)
115115
</t>
116116
<t t-else="">
117-
<t t-esc="state.totalCount" /> Result(s) Found
117+
0 Result(s) Found
118118
</t>
119119
</h5>
120+
<t t-if="totalPages > 1">
121+
<nav aria-label="Search results pagination">
122+
<ul class="pagination pagination-sm mb-0">
123+
<li
124+
t-attf-class="page-item {{ state.currentPage === 1 ? 'disabled' : '' }}"
125+
>
126+
<button
127+
type="button"
128+
class="page-link"
129+
t-on-click="() => this.goToPage(state.currentPage - 1)"
130+
t-att-disabled="state.currentPage === 1"
131+
>
132+
<i
133+
class="fa fa-chevron-left"
134+
aria-hidden="true"
135+
/>
136+
</button>
137+
</li>
138+
<li class="page-item disabled">
139+
<span class="page-link">
140+
<t t-esc="state.currentPage" /> / <t
141+
t-esc="totalPages"
142+
/>
143+
</span>
144+
</li>
145+
<li
146+
t-attf-class="page-item {{ state.currentPage === totalPages ? 'disabled' : '' }}"
147+
>
148+
<button
149+
type="button"
150+
class="page-link"
151+
t-on-click="() => this.goToPage(state.currentPage + 1)"
152+
t-att-disabled="state.currentPage === totalPages"
153+
>
154+
<i
155+
class="fa fa-chevron-right"
156+
aria-hidden="true"
157+
/>
158+
</button>
159+
</li>
160+
</ul>
161+
</nav>
162+
</t>
120163
</div>
121164

122165
<div
@@ -168,16 +211,20 @@
168211
t-esc="getRegistrantTypeLabel(result.is_group)"
169212
/>
170213
</span>
171-
<t t-if="result.phone">
214+
<t
215+
t-if="result.phone_numbers and result.phone_numbers.length"
216+
>
172217
<i
173218
class="fa fa-phone me-1"
174219
aria-hidden="true"
175220
/>
176-
<t t-esc="result.phone" />
221+
<t
222+
t-esc="result.phone_numbers.join(', ')"
223+
/>
177224
</t>
178225
<t t-if="result.email">
179226
<span
180-
t-if="result.phone"
227+
t-if="result.phone_numbers and result.phone_numbers.length"
181228
class="mx-2"
182229
>|</span>
183230
<i

0 commit comments

Comments
 (0)