Skip to content

Commit 9ee1169

Browse files
authored
crud group ownership (#601)
1 parent 2c95384 commit 9ee1169

File tree

15 files changed

+223
-20
lines changed

15 files changed

+223
-20
lines changed

papermerge/core/features/custom_fields/db/api.py

+60-13
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import uuid
44

55
from sqlalchemy import select, func, or_
6-
from sqlalchemy.orm import Session
6+
from sqlalchemy.orm import Session, aliased
77

88

99
from papermerge.core import schema, orm
@@ -17,6 +17,8 @@
1717
"-type": orm.CustomField.type.desc(),
1818
"name": orm.CustomField.name.asc(),
1919
"-name": orm.CustomField.name.desc(),
20+
"group_name": orm.Group.name.asc().nullsfirst(),
21+
"-group_name": orm.Group.name.desc().nullslast(),
2022
}
2123

2224

@@ -29,9 +31,16 @@ def get_custom_fields(
2931
filter: str,
3032
order_by: str = "name",
3133
) -> schema.PaginatedResponse[schema.CustomField]:
34+
35+
UserGroupAlias = aliased(orm.user_groups_association)
36+
subquery = select(UserGroupAlias.c.group_id).where(
37+
UserGroupAlias.c.user_id == user_id
38+
)
39+
3240
stmt_total_cf = select(func.count(orm.CustomField.id)).where(
33-
orm.CustomField.user_id == user_id
41+
or_(orm.CustomField.user_id == user_id, orm.CustomField.group_id.in_(subquery))
3442
)
43+
3544
if filter:
3645
stmt_total_cf = stmt_total_cf.where(
3746
or_(
@@ -45,8 +54,18 @@ def get_custom_fields(
4554

4655
offset = page_size * (page_number - 1)
4756
stmt = (
48-
select(orm.CustomField)
49-
.where(orm.CustomField.user_id == user_id)
57+
select(
58+
orm.CustomField,
59+
orm.Group.name.label("group_name"),
60+
orm.Group.id.label("group_id"),
61+
)
62+
.join(orm.Group, orm.Group.id == orm.CustomField.group_id, isouter=True)
63+
.where(
64+
or_(
65+
orm.CustomField.user_id == user_id,
66+
orm.CustomField.group_id.in_(subquery),
67+
)
68+
)
5069
.limit(page_size)
5170
.offset(offset)
5271
.order_by(order_by_value)
@@ -59,9 +78,20 @@ def get_custom_fields(
5978
orm.CustomField.type.icontains(filter),
6079
)
6180
)
81+
items = []
6282

63-
db_cfs = db_session.scalars(stmt).all()
64-
items = [schema.CustomField.model_validate(db_cf) for db_cf in db_cfs]
83+
for row in db_session.execute(stmt):
84+
kwargs = {
85+
"id": row.CustomField.id,
86+
"name": row.CustomField.name,
87+
"type": row.CustomField.type,
88+
"extra_data": row.CustomField.extra_data,
89+
}
90+
if row.group_name and row.group_id:
91+
kwargs["group_id"] = row.group_id
92+
kwargs["group_name"] = row.group_name
93+
94+
items.append(schema.CustomField(**kwargs))
6595

6696
total_pages = math.ceil(total_cf / page_size)
6797

@@ -127,9 +157,23 @@ def create_custom_field(
127157
def get_custom_field(
128158
session: Session, custom_field_id: uuid.UUID
129159
) -> schema.CustomField:
130-
stmt = select(orm.CustomField).where(orm.CustomField.id == custom_field_id)
131-
db_item = session.scalars(stmt).unique().one()
132-
result = schema.CustomField.model_validate(db_item)
160+
stmt = (
161+
select(orm.CustomField, orm.Group)
162+
.join(orm.Group, orm.Group.id == orm.CustomField.group_id, isouter=True)
163+
.where(orm.CustomField.id == custom_field_id)
164+
)
165+
row = session.execute(stmt).unique().one()
166+
kwargs = {
167+
"id": row.CustomField.id,
168+
"name": row.CustomField.name,
169+
"type": row.CustomField.type,
170+
"extra_data": row.CustomField.extra_data,
171+
}
172+
if row.Group and row.Group.id:
173+
kwargs["group_id"] = row.Group.id
174+
kwargs["group_name"] = row.Group.name
175+
176+
result = schema.CustomField(**kwargs)
133177
return result
134178

135179

@@ -156,13 +200,16 @@ def update_custom_field(
156200
if attrs.extra_data:
157201
cfield.extra_data = attrs.extra_data
158202

159-
if attrs.user_id:
160-
cfield.user_id = attrs.user_id
161-
cfield.group_id = None
162-
163203
if attrs.group_id:
164204
cfield.user_id = None
165205
cfield.group_id = attrs.group_id
206+
elif attrs.user_id:
207+
cfield.user_id = attrs.user_id
208+
cfield.group_id = None
209+
else:
210+
raise ValueError(
211+
"Either attrs.user_id or attrs.group_id should be non-empty value"
212+
)
166213

167214
session.commit()
168215
result = schema.CustomField.model_validate(cfield)

papermerge/core/features/custom_fields/router.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -228,14 +228,16 @@ def update_custom_field(
228228
if attrs.group_id:
229229
group_id = attrs.group_id
230230
ok = user_dbapi.user_belongs_to(
231-
db_session, user_id=attrs.id, group_id=group_id
231+
db_session, user_id=cur_user.id, group_id=group_id
232232
)
233233
if not ok:
234234
user_id = cur_user.id
235235
detail = f"User {user_id=} does not belong to group {group_id=}"
236236
raise HTTPException(
237237
status_code=status.HTTP_403_FORBIDDEN, detail=detail
238238
)
239+
else:
240+
attrs.user_id = cur_user.id
239241
try:
240242
cfield: cf_schema.CustomField = dbapi.update_custom_field(
241243
db_session, custom_field_id=custom_field_id, attrs=attrs

papermerge/core/features/custom_fields/schema.py

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class CustomField(BaseModel):
2525
# Basically `extra_data` is either a stringified JSON i.e. json.dumps(...)
2626
# or an actually python dict (or None)
2727
extra_data: str | dict | None
28+
group_id: UUID | None = None
29+
group_name: str | None = None
2830

2931
# Config
3032
model_config = ConfigDict(from_attributes=True)

papermerge/core/features/custom_fields/tests/test_dbapi_custom_fields.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def test_custom_field_update(db_session: Session, user: User):
4545
dbapi.update_custom_field(
4646
db_session,
4747
custom_field_id=cfield.id,
48-
attrs=schema.UpdateCustomField(name="new_cf1_name"),
48+
attrs=schema.UpdateCustomField(name="new_cf1_name", user_id=user.id),
4949
)
5050

5151
updated_cf1 = dbapi.get_custom_field(db_session, cfield.id)

papermerge/core/features/custom_fields/types.py

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class OrderBy(str, Enum):
88
name_desc = "-name"
99
type_asc = "type"
1010
type_desc = "-type"
11+
owner_asc = "group_name"
12+
owner_desc = "-group_name"
1113

1214

1315
class PaginatedQueryParams(BaseParams):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {useGetUserGroupHomesQuery} from "@/features/users/apiSlice"
2+
3+
import {ComboboxItem, ComboboxItemGroup, Select, Skeleton} from "@mantine/core"
4+
5+
interface Args {
6+
value: ComboboxItem
7+
onChange: (value: ComboboxItem) => void
8+
}
9+
10+
export default function OwnerSelector({onChange, value}: Args) {
11+
const {data, isLoading} = useGetUserGroupHomesQuery()
12+
13+
const onLocalChange = (_value: string | null, option: ComboboxItem) => {
14+
onChange(option)
15+
}
16+
17+
if (isLoading) {
18+
return (
19+
<Skeleton>
20+
<Select />
21+
</Skeleton>
22+
)
23+
}
24+
25+
let owners_data: ComboboxItemGroup[] = [
26+
{
27+
group: "Me",
28+
items: [{value: "", label: "Me"}]
29+
}
30+
]
31+
32+
if (data && data?.length > 0) {
33+
let items = data.map(i => {
34+
return {value: i.group_id, label: i.group_name}
35+
})
36+
owners_data = owners_data.concat({
37+
group: "My Groups",
38+
items: items
39+
})
40+
}
41+
42+
return (
43+
<Select
44+
mt="md"
45+
label={"Owner"}
46+
data={owners_data}
47+
value={value ? value.value : null}
48+
onChange={onLocalChange}
49+
allowDeselect={false}
50+
defaultValue={""}
51+
searchable
52+
checkIconPosition="right"
53+
/>
54+
)
55+
}

ui2/src/components/OwnerSelect/index.tsx

Whitespace-only changes.

ui2/src/features/custom-fields/components/CustomFieldForm.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ export default function CustomFieldForm({customField}: Args) {
3131
data={CUSTOM_FIELD_DATA_TYPES}
3232
onChange={() => {}}
3333
/>
34+
<TextInput
35+
my="md"
36+
label="Owner"
37+
value={customField?.group_name || "Me"}
38+
onChange={() => {}}
39+
rightSection={<CopyButton value={customField?.group_name || "Me"} />}
40+
/>
3441
</Box>
3542
)
3643
}

ui2/src/features/custom-fields/components/CustomFieldRow.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export default function CustomFieldRow({customField}: Args) {
3838
<Table.Td>
3939
<Link to={`/custom-fields/${customField.id}`}>{customField.type}</Link>
4040
</Table.Td>
41+
<Table.Td>{customField.group_name || "Me"}</Table.Td>
4142
</Table.Tr>
4243
)
4344
}

ui2/src/features/custom-fields/components/EditCustomFieldModal.tsx

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import OwnerSelector from "@/components/OwnerSelect/OwnerSelect"
12
import type {CurrencyType, CustomFieldDataType} from "@/types"
23
import {
34
Button,
5+
ComboboxItem,
46
Group,
57
Loader,
68
LoadingOverlay,
@@ -38,6 +40,7 @@ export default function EditGroupModal({
3840
const [dataType, setDataType] = useState<CustomFieldDataType>(
3941
data?.type || "text"
4042
)
43+
const [owner, setOwner] = useState<ComboboxItem>({value: "", label: "Me"})
4144

4245
useEffect(() => {
4346
formReset()
@@ -46,6 +49,11 @@ export default function EditGroupModal({
4649
const formReset = () => {
4750
if (data) {
4851
setName(data.name || "")
52+
if (data.group_id && data.group_name) {
53+
setOwner({value: data.group_id, label: data.group_name})
54+
} else {
55+
setOwner({value: "", label: "Me"})
56+
}
4957
}
5058
}
5159

@@ -56,14 +64,22 @@ export default function EditGroupModal({
5664
extra_data = JSON.stringify({currency: currency})
5765
}
5866

59-
let updatedData = {
67+
const updatedData = {
6068
id: customFieldId,
6169
name: name,
6270
type: dataType,
6371
extra_data: extra_data
6472
}
6573

66-
await updateCustomField(updatedData)
74+
let cfData
75+
if (owner.value && owner.value != "") {
76+
// @ts-ignore
77+
cfData = {...updatedData, group_id: owner.value}
78+
} else {
79+
cfData = updatedData
80+
}
81+
82+
await updateCustomField(cfData)
6783
formReset()
6884
onSubmit()
6985
}
@@ -77,6 +93,10 @@ export default function EditGroupModal({
7793
setCurrency(value as CurrencyType)
7894
}
7995

96+
const onOwnerChange = (option: ComboboxItem) => {
97+
setOwner(option)
98+
}
99+
80100
return (
81101
<Modal
82102
title={"Edit Custom Field"}
@@ -104,6 +124,7 @@ export default function EditGroupModal({
104124
setDataType(e.currentTarget.value as CustomFieldDataType)
105125
}
106126
/>
127+
<OwnerSelector value={owner} onChange={onOwnerChange} />
107128
{dataType == "monetary" && (
108129
<Select
109130
mt="sm"

ui2/src/features/custom-fields/components/List.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import {
77
lastPageSizeUpdate,
88
selectLastPageSize,
99
selectReverseSortedByName,
10+
selectReverseSortedByOwner,
1011
selectReverseSortedByType,
1112
selectSelectedIds,
1213
selectSortedByName,
14+
selectSortedByOwner,
1315
selectSortedByType,
1416
selectTableSortColumns,
1517
selectionAddMany,
@@ -27,8 +29,10 @@ export default function CustomFieldsList() {
2729
const tablerSortCols = useSelector(selectTableSortColumns)
2830
const sortedByName = useSelector(selectSortedByName)
2931
const sortedByType = useSelector(selectSortedByType)
32+
const sortedByOwner = useSelector(selectSortedByOwner)
3033
const reverseSortedByName = useSelector(selectReverseSortedByName)
3134
const reverseSortedByType = useSelector(selectReverseSortedByType)
35+
const reverseSortedByOwner = useSelector(selectReverseSortedByOwner)
3236
const dispatch = useDispatch()
3337
const lastPageSize = useSelector(selectLastPageSize)
3438
const [page, setPage] = useState<number>(1)
@@ -141,6 +145,13 @@ export default function CustomFieldsList() {
141145
>
142146
Type
143147
</Th>
148+
<Th
149+
sorted={sortedByOwner}
150+
reversed={reverseSortedByOwner}
151+
onSort={() => onSortBy("group_name")}
152+
>
153+
Owner
154+
</Th>
144155
</Table.Tr>
145156
</Table.Thead>
146157
<Table.Tbody>{customFieldRows}</Table.Tbody>

0 commit comments

Comments
 (0)