mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-15 02:33:22 +00:00
Remove ajax_select from counters
This commit is contained in:
parent
125157fdf4
commit
7f8a2c1eaf
30
club/static/webpack/club/components/ajax-select-index.ts
Normal file
30
club/static/webpack/club/components/ajax-select-index.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { AjaxSelect } from "#core:core/components/ajax-select-base";
|
||||||
|
import { registerComponent } from "#core:utils/web-components";
|
||||||
|
import type { TomOption } from "tom-select/dist/types/types";
|
||||||
|
import type { escape_html } from "tom-select/dist/types/utils";
|
||||||
|
import { type ClubSchema, clubSearchClub } from "#openapi";
|
||||||
|
|
||||||
|
@registerComponent("club-ajax-select")
|
||||||
|
export class ClubAjaxSelect extends AjaxSelect {
|
||||||
|
protected valueField = "id";
|
||||||
|
protected labelField = "name";
|
||||||
|
protected searchField = ["code", "name"];
|
||||||
|
|
||||||
|
protected async search(query: string): Promise<TomOption[]> {
|
||||||
|
const resp = await clubSearchClub({ query: { search: query } });
|
||||||
|
if (resp.data) {
|
||||||
|
return resp.data.results;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderOption(item: ClubSchema, sanitize: typeof escape_html) {
|
||||||
|
return `<div class="select-item">
|
||||||
|
<span class="select-item-text">${sanitize(item.name)}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderItem(item: ClubSchema, sanitize: typeof escape_html) {
|
||||||
|
return `<span>${sanitize(item.name)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
25
club/widgets/select.py
Normal file
25
club/widgets/select.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from django.forms import Select, SelectMultiple
|
||||||
|
|
||||||
|
from club.models import Club
|
||||||
|
from club.schemas import ClubSchema
|
||||||
|
from core.views.widgets.select import AutoCompleteSelectMixin
|
||||||
|
|
||||||
|
|
||||||
|
class AutoCompleteSelectClub(AutoCompleteSelectMixin, Select):
|
||||||
|
component_name = "club-ajax-select"
|
||||||
|
model = Club
|
||||||
|
schema = ClubSchema
|
||||||
|
|
||||||
|
js = [
|
||||||
|
"webpack/club/components/ajax-select-index.ts",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AutoCompleteSelectMultipleClub(AutoCompleteSelectMixin, SelectMultiple):
|
||||||
|
component_name = "club-ajax-select"
|
||||||
|
model = Club
|
||||||
|
schema = ClubSchema
|
||||||
|
|
||||||
|
js = [
|
||||||
|
"webpack/club/components/ajax-select-index.ts",
|
||||||
|
]
|
15
core/api.py
15
core/api.py
@ -15,9 +15,10 @@ from core.api_permissions import (
|
|||||||
CanAccessLookup,
|
CanAccessLookup,
|
||||||
CanView,
|
CanView,
|
||||||
)
|
)
|
||||||
from core.models import SithFile, User
|
from core.models import Group, SithFile, User
|
||||||
from core.schemas import (
|
from core.schemas import (
|
||||||
FamilyGodfatherSchema,
|
FamilyGodfatherSchema,
|
||||||
|
GroupSchema,
|
||||||
MarkdownSchema,
|
MarkdownSchema,
|
||||||
SithFileSchema,
|
SithFileSchema,
|
||||||
UserFamilySchema,
|
UserFamilySchema,
|
||||||
@ -78,6 +79,18 @@ class SithFileController(ControllerBase):
|
|||||||
return SithFile.objects.filter(is_in_sas=False).filter(name__icontains=query)
|
return SithFile.objects.filter(is_in_sas=False).filter(name__icontains=query)
|
||||||
|
|
||||||
|
|
||||||
|
@api_controller("/group")
|
||||||
|
class GroupController(ControllerBase):
|
||||||
|
@route.get(
|
||||||
|
"/search",
|
||||||
|
response=PaginatedResponseSchema[GroupSchema],
|
||||||
|
permissions=[CanAccessLookup],
|
||||||
|
)
|
||||||
|
@paginate(PageNumberPaginationExtra, page_size=50)
|
||||||
|
def search_group(self, search: Annotated[str, annotated_types.MinLen(1)]):
|
||||||
|
return Group.objects.filter(name__icontains=search).values()
|
||||||
|
|
||||||
|
|
||||||
DepthValue = Annotated[int, annotated_types.Ge(0), annotated_types.Le(10)]
|
DepthValue = Annotated[int, annotated_types.Ge(0), annotated_types.Le(10)]
|
||||||
DEFAULT_DEPTH = 4
|
DEFAULT_DEPTH = 4
|
||||||
|
|
||||||
|
184
core/static/webpack/core/components/ajax-select-base.ts
Normal file
184
core/static/webpack/core/components/ajax-select-base.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import { inheritHtmlElement } from "#core:utils/web-components";
|
||||||
|
import TomSelect from "tom-select";
|
||||||
|
import type {
|
||||||
|
RecursivePartial,
|
||||||
|
TomLoadCallback,
|
||||||
|
TomOption,
|
||||||
|
TomSettings,
|
||||||
|
} from "tom-select/dist/types/types";
|
||||||
|
import type { escape_html } from "tom-select/dist/types/utils";
|
||||||
|
|
||||||
|
export class AutoCompleteSelectBase extends inheritHtmlElement("select") {
|
||||||
|
static observedAttributes = [
|
||||||
|
"delay",
|
||||||
|
"placeholder",
|
||||||
|
"max",
|
||||||
|
"min-characters-for-search",
|
||||||
|
];
|
||||||
|
public widget: TomSelect;
|
||||||
|
|
||||||
|
protected minCharNumberForSearch = 0;
|
||||||
|
protected delay: number | null = null;
|
||||||
|
protected placeholder = "";
|
||||||
|
protected max: number | null = null;
|
||||||
|
|
||||||
|
protected attributeChangedCallback(
|
||||||
|
name: string,
|
||||||
|
_oldValue?: string,
|
||||||
|
newValue?: string,
|
||||||
|
) {
|
||||||
|
switch (name) {
|
||||||
|
case "delay": {
|
||||||
|
this.delay = Number.parseInt(newValue) ?? null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "placeholder": {
|
||||||
|
this.placeholder = newValue ?? "";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "max": {
|
||||||
|
this.max = Number.parseInt(newValue) ?? null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "min-characters-for-search": {
|
||||||
|
this.minCharNumberForSearch = Number.parseInt(newValue) ?? 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.widget = new TomSelect(this.node, this.tomSelectSettings());
|
||||||
|
this.attachBehaviors();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected shouldLoad(query: string) {
|
||||||
|
return query.length >= this.minCharNumberForSearch; // Avoid launching search with less than setup number of characters
|
||||||
|
}
|
||||||
|
|
||||||
|
protected tomSelectSettings(): RecursivePartial<TomSettings> {
|
||||||
|
return {
|
||||||
|
plugins: {
|
||||||
|
// biome-ignore lint/style/useNamingConvention: this is required by the api
|
||||||
|
remove_button: {
|
||||||
|
title: gettext("Remove"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
persist: false,
|
||||||
|
maxItems: this.node.multiple ? this.max : 1,
|
||||||
|
closeAfterSelect: true,
|
||||||
|
loadThrottle: this.delay,
|
||||||
|
placeholder: this.placeholder,
|
||||||
|
shouldLoad: (query: string) => this.shouldLoad(query), // wraps the method to avoid shadowing `this` by the one from tom-select
|
||||||
|
render: {
|
||||||
|
option: (item: TomOption, sanitize: typeof escape_html) => {
|
||||||
|
return `<div class="select-item">
|
||||||
|
<span class="select-item-text">${sanitize(item.text)}</span>
|
||||||
|
</div>`;
|
||||||
|
},
|
||||||
|
item: (item: TomOption, sanitize: typeof escape_html) => {
|
||||||
|
return `<span>${sanitize(item.text)}</span>`;
|
||||||
|
},
|
||||||
|
// biome-ignore lint/style/useNamingConvention: that's how it's defined
|
||||||
|
not_loading: (data: TomOption, _sanitize: typeof escape_html) => {
|
||||||
|
return `<div class="no-results">${interpolate(gettext("You need to type %(number)s more characters"), { number: this.minCharNumberForSearch - data.input.length }, true)}</div>`;
|
||||||
|
},
|
||||||
|
// biome-ignore lint/style/useNamingConvention: that's how it's defined
|
||||||
|
no_results: (_data: TomOption, _sanitize: typeof escape_html) => {
|
||||||
|
return `<div class="no-results">${gettext("No results found")}</div>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected attachBehaviors() {
|
||||||
|
/* Called once the widget has been initialized */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class AjaxSelect extends AutoCompleteSelectBase {
|
||||||
|
protected filter?: (items: TomOption[]) => TomOption[] = null;
|
||||||
|
protected minCharNumberForSearch = 2;
|
||||||
|
|
||||||
|
protected abstract valueField: string;
|
||||||
|
protected abstract labelField: string;
|
||||||
|
protected abstract searchField: string[];
|
||||||
|
|
||||||
|
protected abstract renderOption(
|
||||||
|
item: TomOption,
|
||||||
|
sanitize: typeof escape_html,
|
||||||
|
): string;
|
||||||
|
protected abstract renderItem(item: TomOption, sanitize: typeof escape_html): string;
|
||||||
|
protected abstract search(query: string): Promise<TomOption[]>;
|
||||||
|
|
||||||
|
private initialValues: TomOption[] = [];
|
||||||
|
public setFilter(filter?: (items: TomOption[]) => TomOption[]) {
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected shouldLoad(query: string) {
|
||||||
|
const resp = super.shouldLoad(query);
|
||||||
|
/* Force order sync with backend if no client side filtering is set */
|
||||||
|
if (!resp && this.searchField.length === 0) {
|
||||||
|
this.widget.clearOptions();
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async loadFunction(query: string, callback: TomLoadCallback) {
|
||||||
|
/* Force order sync with backend if no client side filtering is set */
|
||||||
|
if (this.searchField.length === 0) {
|
||||||
|
this.widget.clearOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await this.search(query);
|
||||||
|
|
||||||
|
if (this.filter) {
|
||||||
|
callback(this.filter(resp), []);
|
||||||
|
} else {
|
||||||
|
callback(resp, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected tomSelectSettings(): RecursivePartial<TomSettings> {
|
||||||
|
return {
|
||||||
|
...super.tomSelectSettings(),
|
||||||
|
hideSelected: true,
|
||||||
|
diacritics: true,
|
||||||
|
duplicates: false,
|
||||||
|
valueField: this.valueField,
|
||||||
|
labelField: this.labelField,
|
||||||
|
searchField: this.searchField,
|
||||||
|
load: (query: string, callback: TomLoadCallback) =>
|
||||||
|
this.loadFunction(query, callback), // wraps the method to avoid shadowing `this` by the one from tom-select
|
||||||
|
render: {
|
||||||
|
...super.tomSelectSettings().render,
|
||||||
|
option: this.renderOption,
|
||||||
|
item: this.renderItem,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
/* Capture initial values before they get moved to the inner node and overridden by tom-select */
|
||||||
|
this.initialValues = Array.from(this.children)
|
||||||
|
.filter((child: Element) => child.tagName.toLowerCase() === "slot")
|
||||||
|
.map((slot) => JSON.parse(slot.innerHTML));
|
||||||
|
|
||||||
|
super.connectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected attachBehaviors() {
|
||||||
|
super.attachBehaviors();
|
||||||
|
|
||||||
|
// Gather selected options, they must be added with slots like `<slot>json</slot>`
|
||||||
|
for (const value of this.initialValues) {
|
||||||
|
this.widget.addOption(value, false);
|
||||||
|
this.widget.addItem(value[this.valueField]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,6 @@
|
|||||||
import "tom-select/dist/css/tom-select.default.css";
|
import "tom-select/dist/css/tom-select.default.css";
|
||||||
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components";
|
import { registerComponent } from "#core:utils/web-components";
|
||||||
import TomSelect from "tom-select";
|
import type { TomOption } from "tom-select/dist/types/types";
|
||||||
import type {
|
|
||||||
RecursivePartial,
|
|
||||||
TomLoadCallback,
|
|
||||||
TomOption,
|
|
||||||
TomSettings,
|
|
||||||
} from "tom-select/dist/types/types";
|
|
||||||
import type { escape_html } from "tom-select/dist/types/utils";
|
import type { escape_html } from "tom-select/dist/types/utils";
|
||||||
import {
|
import {
|
||||||
type GroupSchema,
|
type GroupSchema,
|
||||||
@ -15,181 +9,13 @@ import {
|
|||||||
userSearchUsers,
|
userSearchUsers,
|
||||||
} from "#openapi";
|
} from "#openapi";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AjaxSelect,
|
||||||
|
AutoCompleteSelectBase,
|
||||||
|
} from "#core:core/components/ajax-select-base";
|
||||||
|
|
||||||
@registerComponent("autocomplete-select")
|
@registerComponent("autocomplete-select")
|
||||||
class AutocompleteSelect extends inheritHtmlElement("select") {
|
export class AutoCompleteSelect extends AutoCompleteSelectBase {}
|
||||||
static observedAttributes = [
|
|
||||||
"delay",
|
|
||||||
"placeholder",
|
|
||||||
"max",
|
|
||||||
"min-characters-for-search",
|
|
||||||
];
|
|
||||||
public widget: TomSelect;
|
|
||||||
|
|
||||||
protected minCharNumberForSearch = 0;
|
|
||||||
protected delay: number | null = null;
|
|
||||||
protected placeholder = "";
|
|
||||||
protected max: number | null = null;
|
|
||||||
|
|
||||||
protected attributeChangedCallback(
|
|
||||||
name: string,
|
|
||||||
_oldValue?: string,
|
|
||||||
newValue?: string,
|
|
||||||
) {
|
|
||||||
switch (name) {
|
|
||||||
case "delay": {
|
|
||||||
this.delay = Number.parseInt(newValue) ?? null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "placeholder": {
|
|
||||||
this.placeholder = newValue ?? "";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "max": {
|
|
||||||
this.max = Number.parseInt(newValue) ?? null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "min-characters-for-search": {
|
|
||||||
this.minCharNumberForSearch = Number.parseInt(newValue) ?? 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this.widget = new TomSelect(this.node, this.tomSelectSettings());
|
|
||||||
this.attachBehaviors();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected shouldLoad(query: string) {
|
|
||||||
return query.length >= this.minCharNumberForSearch; // Avoid launching search with less than setup number of characters
|
|
||||||
}
|
|
||||||
|
|
||||||
protected tomSelectSettings(): RecursivePartial<TomSettings> {
|
|
||||||
return {
|
|
||||||
plugins: {
|
|
||||||
// biome-ignore lint/style/useNamingConvention: this is required by the api
|
|
||||||
remove_button: {
|
|
||||||
title: gettext("Remove"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
persist: false,
|
|
||||||
maxItems: this.node.multiple ? this.max : 1,
|
|
||||||
closeAfterSelect: true,
|
|
||||||
loadThrottle: this.delay,
|
|
||||||
placeholder: this.placeholder,
|
|
||||||
shouldLoad: (query: string) => this.shouldLoad(query), // wraps the method to avoid shadowing `this` by the one from tom-select
|
|
||||||
render: {
|
|
||||||
option: (item: TomOption, sanitize: typeof escape_html) => {
|
|
||||||
return `<div class="select-item">
|
|
||||||
<span class="select-item-text">${sanitize(item.text)}</span>
|
|
||||||
</div>`;
|
|
||||||
},
|
|
||||||
item: (item: TomOption, sanitize: typeof escape_html) => {
|
|
||||||
return `<span>${sanitize(item.text)}</span>`;
|
|
||||||
},
|
|
||||||
// biome-ignore lint/style/useNamingConvention: that's how it's defined
|
|
||||||
not_loading: (data: TomOption, _sanitize: typeof escape_html) => {
|
|
||||||
return `<div class="no-results">${interpolate(gettext("You need to type %(number)s more characters"), { number: this.minCharNumberForSearch - data.input.length }, true)}</div>`;
|
|
||||||
},
|
|
||||||
// biome-ignore lint/style/useNamingConvention: that's how it's defined
|
|
||||||
no_results: (_data: TomOption, _sanitize: typeof escape_html) => {
|
|
||||||
return `<div class="no-results">${gettext("No results found")}</div>`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected attachBehaviors() {
|
|
||||||
/* Called once the widget has been initialized */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class AjaxSelect extends AutocompleteSelect {
|
|
||||||
protected filter?: (items: TomOption[]) => TomOption[] = null;
|
|
||||||
protected minCharNumberForSearch = 2;
|
|
||||||
|
|
||||||
protected abstract valueField: string;
|
|
||||||
protected abstract labelField: string;
|
|
||||||
protected abstract searchField: string[];
|
|
||||||
|
|
||||||
protected abstract renderOption(
|
|
||||||
item: TomOption,
|
|
||||||
sanitize: typeof escape_html,
|
|
||||||
): string;
|
|
||||||
protected abstract renderItem(item: TomOption, sanitize: typeof escape_html): string;
|
|
||||||
protected abstract search(query: string): Promise<TomOption[]>;
|
|
||||||
|
|
||||||
private initialValues: TomOption[] = [];
|
|
||||||
public setFilter(filter?: (items: TomOption[]) => TomOption[]) {
|
|
||||||
this.filter = filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected shouldLoad(query: string) {
|
|
||||||
const resp = super.shouldLoad(query);
|
|
||||||
/* Force order sync with backend if no client side filtering is set */
|
|
||||||
if (!resp && this.searchField.length === 0) {
|
|
||||||
this.widget.clearOptions();
|
|
||||||
}
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async loadFunction(query: string, callback: TomLoadCallback) {
|
|
||||||
/* Force order sync with backend if no client side filtering is set */
|
|
||||||
if (this.searchField.length === 0) {
|
|
||||||
this.widget.clearOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp = await this.search(query);
|
|
||||||
|
|
||||||
if (this.filter) {
|
|
||||||
callback(this.filter(resp), []);
|
|
||||||
} else {
|
|
||||||
callback(resp, []);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected tomSelectSettings(): RecursivePartial<TomSettings> {
|
|
||||||
return {
|
|
||||||
...super.tomSelectSettings(),
|
|
||||||
hideSelected: true,
|
|
||||||
diacritics: true,
|
|
||||||
duplicates: false,
|
|
||||||
valueField: this.valueField,
|
|
||||||
labelField: this.labelField,
|
|
||||||
searchField: this.searchField,
|
|
||||||
load: (query: string, callback: TomLoadCallback) =>
|
|
||||||
this.loadFunction(query, callback), // wraps the method to avoid shadowing `this` by the one from tom-select
|
|
||||||
render: {
|
|
||||||
...super.tomSelectSettings().render,
|
|
||||||
option: this.renderOption,
|
|
||||||
item: this.renderItem,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
/* Capture initial values before they get moved to the inner node and overridden by tom-select */
|
|
||||||
this.initialValues = Array.from(this.children)
|
|
||||||
.filter((child) => child.tagName.toLowerCase() === "slot")
|
|
||||||
.map((slot) => JSON.parse(slot.innerHTML));
|
|
||||||
|
|
||||||
super.connectedCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected attachBehaviors() {
|
|
||||||
super.attachBehaviors();
|
|
||||||
|
|
||||||
// Gather selected options, they must be added with slots like `<slot>json</slot>`
|
|
||||||
for (const value of this.initialValues) {
|
|
||||||
this.widget.addOption(value, false);
|
|
||||||
this.widget.addItem(value[this.valueField]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@registerComponent("user-ajax-select")
|
@registerComponent("user-ajax-select")
|
||||||
export class UserAjaxSelect extends AjaxSelect {
|
export class UserAjaxSelect extends AjaxSelect {
|
||||||
|
@ -49,7 +49,15 @@ class AutoCompleteSelectMixin:
|
|||||||
context["selected"] = [
|
context["selected"] = [
|
||||||
self.schema.from_orm(obj).json()
|
self.schema.from_orm(obj).json()
|
||||||
for obj in self.model.objects.filter(
|
for obj in self.model.objects.filter(
|
||||||
**{f"{self.pk}__in": context["widget"]["value"]}
|
**{
|
||||||
|
f"{self.pk}__in": [
|
||||||
|
pk
|
||||||
|
for pk in context["widget"]["value"]
|
||||||
|
if str(
|
||||||
|
pk
|
||||||
|
).isdigit() # We filter empty values for create views
|
||||||
|
]
|
||||||
|
}
|
||||||
).all()
|
).all()
|
||||||
]
|
]
|
||||||
return context
|
return context
|
||||||
|
@ -22,8 +22,6 @@ from ninja_extra.pagination import PageNumberPaginationExtra
|
|||||||
from ninja_extra.schemas import PaginatedResponseSchema
|
from ninja_extra.schemas import PaginatedResponseSchema
|
||||||
|
|
||||||
from core.api_permissions import CanAccessLookup, CanView, IsRoot
|
from core.api_permissions import CanAccessLookup, CanView, IsRoot
|
||||||
from core.models import Group
|
|
||||||
from core.schemas import GroupSchema
|
|
||||||
from counter.models import Counter, Product
|
from counter.models import Counter, Product
|
||||||
from counter.schemas import (
|
from counter.schemas import (
|
||||||
CounterFilterSchema,
|
CounterFilterSchema,
|
||||||
@ -78,15 +76,3 @@ class ProductController(ControllerBase):
|
|||||||
.filter(archived=False)
|
.filter(archived=False)
|
||||||
.values()
|
.values()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@api_controller("/group")
|
|
||||||
class GroupController(ControllerBase):
|
|
||||||
@route.get(
|
|
||||||
"/search",
|
|
||||||
response=PaginatedResponseSchema[GroupSchema],
|
|
||||||
permissions=[CanAccessLookup],
|
|
||||||
)
|
|
||||||
@paginate(PageNumberPaginationExtra, page_size=50)
|
|
||||||
def search_group(self, search: Annotated[str, MinLen(1)]):
|
|
||||||
return Group.objects.filter(name__icontains=search).values()
|
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
from ajax_select import make_ajax_field
|
|
||||||
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||||
|
|
||||||
|
from club.widgets.select import AutoCompleteSelectClub
|
||||||
from core.views.forms import NFCTextInput, SelectDate, SelectDateTime
|
from core.views.forms import NFCTextInput, SelectDate, SelectDateTime
|
||||||
|
from core.views.widgets.select import (
|
||||||
|
AutoCompleteSelect,
|
||||||
|
AutoCompleteSelectMultipleGroup,
|
||||||
|
AutoCompleteSelectMultipleUser,
|
||||||
|
AutoCompleteSelectUser,
|
||||||
|
)
|
||||||
from counter.models import (
|
from counter.models import (
|
||||||
BillingInfo,
|
BillingInfo,
|
||||||
Counter,
|
Counter,
|
||||||
@ -14,6 +19,11 @@ from counter.models import (
|
|||||||
Refilling,
|
Refilling,
|
||||||
StudentCard,
|
StudentCard,
|
||||||
)
|
)
|
||||||
|
from counter.widgets.select import (
|
||||||
|
AutoCompleteSelectMultipleCounter,
|
||||||
|
AutoCompleteSelectMultipleProduct,
|
||||||
|
AutoCompleteSelectProduct,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BillingInfoForm(forms.ModelForm):
|
class BillingInfoForm(forms.ModelForm):
|
||||||
@ -68,8 +78,11 @@ class GetUserForm(forms.Form):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=NFCTextInput,
|
widget=NFCTextInput,
|
||||||
)
|
)
|
||||||
id = AutoCompleteSelectField(
|
id = forms.CharField(
|
||||||
"users", required=False, label=_("Select user"), help_text=None
|
label=_("Select user"),
|
||||||
|
help_text=None,
|
||||||
|
widget=AutoCompleteSelectUser,
|
||||||
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def as_p(self):
|
def as_p(self):
|
||||||
@ -122,8 +135,10 @@ class CounterEditForm(forms.ModelForm):
|
|||||||
model = Counter
|
model = Counter
|
||||||
fields = ["sellers", "products"]
|
fields = ["sellers", "products"]
|
||||||
|
|
||||||
sellers = make_ajax_field(Counter, "sellers", "users", help_text="")
|
widgets = {
|
||||||
products = make_ajax_field(Counter, "products", "products", help_text="")
|
"sellers": AutoCompleteSelectMultipleUser,
|
||||||
|
"products": AutoCompleteSelectMultipleProduct,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProductEditForm(forms.ModelForm):
|
class ProductEditForm(forms.ModelForm):
|
||||||
@ -145,44 +160,37 @@ class ProductEditForm(forms.ModelForm):
|
|||||||
"tray",
|
"tray",
|
||||||
"archived",
|
"archived",
|
||||||
]
|
]
|
||||||
|
widgets = {
|
||||||
|
"parent_product": AutoCompleteSelectMultipleProduct,
|
||||||
|
"product_type": AutoCompleteSelect,
|
||||||
|
"buying_groups": AutoCompleteSelectMultipleGroup,
|
||||||
|
"club": AutoCompleteSelectClub,
|
||||||
|
}
|
||||||
|
|
||||||
parent_product = AutoCompleteSelectField(
|
counters = forms.ModelMultipleChoiceField(
|
||||||
"products", show_help_text=False, label=_("Parent product"), required=False
|
help_text=None,
|
||||||
)
|
|
||||||
buying_groups = AutoCompleteSelectMultipleField(
|
|
||||||
"groups",
|
|
||||||
show_help_text=False,
|
|
||||||
help_text="",
|
|
||||||
label=_("Buying groups"),
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
club = AutoCompleteSelectField("clubs", show_help_text=False)
|
|
||||||
counters = AutoCompleteSelectMultipleField(
|
|
||||||
"counters",
|
|
||||||
show_help_text=False,
|
|
||||||
help_text="",
|
|
||||||
label=_("Counters"),
|
label=_("Counters"),
|
||||||
required=False,
|
required=False,
|
||||||
|
widget=AutoCompleteSelectMultipleCounter,
|
||||||
|
queryset=Counter.objects.all(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if self.instance.id:
|
if self.instance.id:
|
||||||
self.fields["counters"].initial = [
|
self.fields["counters"].initial = self.instance.counters.all()
|
||||||
str(c.id) for c in self.instance.counters.all()
|
|
||||||
]
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
ret = super().save(*args, **kwargs)
|
ret = super().save(*args, **kwargs)
|
||||||
if self.fields["counters"].initial:
|
if self.fields["counters"].initial:
|
||||||
for cid in self.fields["counters"].initial:
|
# Remove the product from all counter it was added to
|
||||||
c = Counter.objects.filter(id=int(cid)).first()
|
# It will then only be added to selected counters
|
||||||
c.products.remove(self.instance)
|
for counter in self.fields["counters"].initial:
|
||||||
c.save()
|
counter.products.remove(self.instance)
|
||||||
for cid in self.cleaned_data["counters"]:
|
counter.save()
|
||||||
c = Counter.objects.filter(id=int(cid)).first()
|
for counter in self.cleaned_data["counters"]:
|
||||||
c.products.add(self.instance)
|
counter.products.add(self.instance)
|
||||||
c.save()
|
counter.save()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@ -199,8 +207,7 @@ class EticketForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Eticket
|
model = Eticket
|
||||||
fields = ["product", "banner", "event_title", "event_date"]
|
fields = ["product", "banner", "event_title", "event_date"]
|
||||||
widgets = {"event_date": SelectDate}
|
widgets = {
|
||||||
|
"product": AutoCompleteSelectProduct,
|
||||||
product = AutoCompleteSelectField(
|
"event_date": SelectDate,
|
||||||
"products", show_help_text=False, label=_("Product"), required=True
|
}
|
||||||
)
|
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
import { AjaxSelect } from "#core:core/components/ajax-select-base";
|
||||||
|
import { registerComponent } from "#core:utils/web-components";
|
||||||
|
import type { TomOption } from "tom-select/dist/types/types";
|
||||||
|
import type { escape_html } from "tom-select/dist/types/utils";
|
||||||
|
import {
|
||||||
|
type CounterSchema,
|
||||||
|
type ProductSchema,
|
||||||
|
counterSearchCounter,
|
||||||
|
productSearchProducts,
|
||||||
|
} from "#openapi";
|
||||||
|
|
||||||
|
@registerComponent("product-ajax-select")
|
||||||
|
export class ProductAjaxSelect extends AjaxSelect {
|
||||||
|
protected valueField = "id";
|
||||||
|
protected labelField = "name";
|
||||||
|
protected searchField = ["code", "name"];
|
||||||
|
|
||||||
|
protected async search(query: string): Promise<TomOption[]> {
|
||||||
|
const resp = await productSearchProducts({ query: { search: query } });
|
||||||
|
if (resp.data) {
|
||||||
|
return resp.data.results;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderOption(item: ProductSchema, sanitize: typeof escape_html) {
|
||||||
|
return `<div class="select-item">
|
||||||
|
<span class="select-item-text">${sanitize(item.code)} - ${sanitize(item.name)}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderItem(item: ProductSchema, sanitize: typeof escape_html) {
|
||||||
|
return `<span>${sanitize(item.code)} - ${sanitize(item.name)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@registerComponent("counter-ajax-select")
|
||||||
|
export class CounterAjaxSelect extends AjaxSelect {
|
||||||
|
protected valueField = "id";
|
||||||
|
protected labelField = "name";
|
||||||
|
protected searchField = ["code", "name"];
|
||||||
|
|
||||||
|
protected async search(query: string): Promise<TomOption[]> {
|
||||||
|
const resp = await counterSearchCounter({ query: { search: query } });
|
||||||
|
if (resp.data) {
|
||||||
|
return resp.data.results;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderOption(item: CounterSchema, sanitize: typeof escape_html) {
|
||||||
|
return `<div class="select-item">
|
||||||
|
<span class="select-item-text">${sanitize(item.name)}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderItem(item: CounterSchema, sanitize: typeof escape_html) {
|
||||||
|
return `<span>${sanitize(item.name)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
35
counter/widgets/select.py
Normal file
35
counter/widgets/select.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from django.forms import Select, SelectMultiple
|
||||||
|
|
||||||
|
from core.views.widgets.select import AutoCompleteSelectMixin
|
||||||
|
from counter.models import Counter, Product
|
||||||
|
from counter.schemas import ProductSchema, SimplifiedCounterSchema
|
||||||
|
|
||||||
|
|
||||||
|
class CounterAutoCompleteSelectMixin(AutoCompleteSelectMixin):
|
||||||
|
js = [
|
||||||
|
"webpack/counter/components/ajax-select-index.ts",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AutoCompleteSelectCounter(CounterAutoCompleteSelectMixin, Select):
|
||||||
|
component_name = "counter-ajax-select"
|
||||||
|
model = Counter
|
||||||
|
schema = SimplifiedCounterSchema
|
||||||
|
|
||||||
|
|
||||||
|
class AutoCompleteSelectMultipleCounter(CounterAutoCompleteSelectMixin, SelectMultiple):
|
||||||
|
component_name = "counter-ajax-select"
|
||||||
|
model = Counter
|
||||||
|
schema = SimplifiedCounterSchema
|
||||||
|
|
||||||
|
|
||||||
|
class AutoCompleteSelectProduct(CounterAutoCompleteSelectMixin, Select):
|
||||||
|
component_name = "product-ajax-select"
|
||||||
|
model = Product
|
||||||
|
schema = ProductSchema
|
||||||
|
|
||||||
|
|
||||||
|
class AutoCompleteSelectMultipleProduct(CounterAutoCompleteSelectMixin, SelectMultiple):
|
||||||
|
component_name = "product-ajax-select"
|
||||||
|
model = Product
|
||||||
|
schema = ProductSchema
|
Loading…
Reference in New Issue
Block a user