Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch Edit: Support for editing basic fields #5417

Open
wants to merge 54 commits into
base: production
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
84229f2
Add workbench migrations
sharadsw Nov 25, 2024
da7cf87
Update workbench models
sharadsw Nov 25, 2024
35a4858
Add batch edit permissions
sharadsw Nov 25, 2024
8584d77
Make a single migration
sharadsw Nov 25, 2024
439b3b6
Add css for batch edit cells
sharadsw Nov 25, 2024
020ed03
Add batch edit in sidebar
sharadsw Nov 25, 2024
b4bcf44
Add permission definitions
sharadsw Nov 26, 2024
86d2e4a
Add menu item definitions
sharadsw Nov 26, 2024
aa641f4
Add batch edit to Queries
sharadsw Nov 26, 2024
d1aab76
Add batch edit localizations
sharadsw Nov 26, 2024
1e6abff
Workbench localizations for batch edit
sharadsw Nov 26, 2024
9fa7373
Add variants for wb and batch edit
sharadsw Nov 26, 2024
bcad157
Add back tectonic tree permission
sharadsw Nov 26, 2024
db53d43
keep isStrict
sharadsw Nov 26, 2024
e0fea3c
Fix lost translations
sharadsw Nov 26, 2024
19f743b
Add batch edit route
sharadsw Nov 27, 2024
c8c7f0e
Use variant localization in workbench
sharadsw Nov 27, 2024
5f32260
Add WbToolkit changes
sharadsw Dec 2, 2024
82f371d
Add WbUtils changes
sharadsw Dec 2, 2024
bdfa96e
Add tailwind changes
sharadsw Dec 2, 2024
5016d11
Add /WorkBench changes
sharadsw Dec 2, 2024
7d9cff8
Add batch edit sort config cache var
sharadsw Dec 2, 2024
55aaaa1
merge prod
sharadsw Dec 9, 2024
439aef4
attachment urls
sharadsw Dec 19, 2024
a7f4637
DataModel changes
sharadsw Dec 19, 2024
25eef3f
Backend code for batch edit
sharadsw Dec 23, 2024
82886ea
Temporarily disable relationships in batch edit
sharadsw Dec 23, 2024
0b3dc37
Temporarily hide data mapper
sharadsw Dec 23, 2024
3b5e6a1
Fix types
sharadsw Dec 23, 2024
37cbaa4
Update unique fields
sharadsw Dec 24, 2024
6b2aed7
Merge remote-tracking branch 'origin/production' into issue-5413
sharadsw Jan 2, 2025
be512b8
Add collection as a param - needed after #5489
sharadsw Jan 3, 2025
bc588c8
Fix typecheck errors
sharadsw Jan 6, 2025
b362060
Reanimate TreeRankQuery
sharadsw Jan 6, 2025
635d649
Remove init.py
sharadsw Jan 6, 2025
3b6de29
Fix tests import
sharadsw Jan 6, 2025
06f21a3
rename test
sharadsw Jan 6, 2025
7040f28
Fix formatter and queryfield tests
sharadsw Jan 6, 2025
85a6dec
Fix import
sharadsw Jan 6, 2025
c9a3f03
Add _update function
sharadsw Jan 7, 2025
ce9582d
Delete test file
sharadsw Jan 7, 2025
b4f8ff1
Add prop for omit relationships
sharadsw Jan 7, 2025
4347fe5
prop fixes and import errors
sharadsw Jan 7, 2025
8ccf573
Hide batch edit documentation url
sharadsw Jan 10, 2025
78dba26
Make age readonly
sharadsw Jan 10, 2025
3f48248
Fix expected errors
sharadsw Jan 10, 2025
0c31f75
Correct bulk_move openapi documentaiton
melton-jason Jan 10, 2025
67a55da
Add placeholder documentation url
sharadsw Jan 10, 2025
01e315b
Merge remote-tracking branch 'origin/production' into issue-5413
sharadsw Jan 10, 2025
4a41ed2
Localization improvements
sharadsw Jan 10, 2025
4d82298
Merge branch 'production' into issue-5413
sharadsw Jan 14, 2025
12d1c72
Remove unused localizations
sharadsw Jan 14, 2025
9fdbf4d
Fix results
sharadsw Jan 16, 2025
b68f9a3
Merge remote-tracking branch 'origin/issue-5413' into issue-5413
sharadsw Jan 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions specifyweb/export/dwca.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from xml.etree import ElementTree as ET
from xml.dom import minidom

from specifyweb.stored_queries.execution import EphemeralField, query_to_csv
from specifyweb.stored_queries.queryfield import QueryField
from specifyweb.stored_queries.execution import query_to_csv
from specifyweb.stored_queries.queryfield import QueryField, EphemeralField
from specifyweb.stored_queries.models import session_context

logger = logging.getLogger(__name__)
Expand Down
147 changes: 93 additions & 54 deletions specifyweb/express_search/related.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,67 +5,79 @@
import logging

from ..specify.models import datamodel
from ..stored_queries.execution import build_query
from ..stored_queries.execution import BuildQueryProps, build_query
from ..stored_queries.query_ops import QueryOps
from ..stored_queries.queryfield import QueryField
from ..stored_queries.queryfieldspec import QueryFieldSpec

logger = logging.getLogger(__name__)


class F(str):
pass


class RelatedSearchMeta(type):
def __new__(cls, name, bases, dict):
Rs = super(RelatedSearchMeta, cls).__new__(cls, name, bases, dict)
if Rs.definitions is None:
return Rs

root_table_name = Rs.definitions[0].split('.')[0]
root_table_name = Rs.definitions[0].split(".")[0]

Rs.root = datamodel.get_table(root_table_name, strict=True)

def col_to_fs(col, add_id=False):
return QueryFieldSpec.from_path( [root_table_name] + col.split('.'), add_id )
return QueryFieldSpec.from_path([root_table_name] + col.split("."), add_id)

Rs.display_fields = [
QueryField(fieldspec=col_to_fs(col),
op_num=1,
value="",
negate=False,
display=True,
format_name=None,
sort_type=0,
strict=False)
for col in Rs.columns]

if Rs.link:
Rs.display_fields.append(QueryField(
fieldspec=col_to_fs(Rs.link, add_id=True),
QueryField(
fieldspec=col_to_fs(col),
op_num=1,
value="",
negate=False,
display=True,
format_name=None,
sort_type=0,
strict=False))
strict=False
)
for col in Rs.columns
]

if Rs.link:
Rs.display_fields.append(
QueryField(
fieldspec=col_to_fs(Rs.link, add_id=True),
op_num=1,
value="",
negate=False,
display=True,
format_name=None,
sort_type=0,
strict=False
)
)

def make_filter(f, negate):
field, op, val = f
return QueryField(fieldspec=col_to_fs(field),
op_num=QueryOps.OPERATIONS.index(op.__name__),
value=col_to_fs(val) if isinstance(val, F) else val,
negate=negate,
display=False,
format_name=None,
sort_type=0,
strict=False)

Rs.filter_fields = [make_filter(f, False) for f in Rs.filters] + \
[make_filter(f, True) for f in Rs.excludes]
return QueryField(
fieldspec=col_to_fs(field),
op_num=QueryOps.OPERATIONS.index(op.__name__),
value=col_to_fs(val) if isinstance(val, F) else val,
negate=negate,
display=False,
format_name=None,
sort_type=0,
strict=False
)

Rs.filter_fields = [make_filter(f, False) for f in Rs.filters] + [
make_filter(f, True) for f in Rs.excludes
]

return Rs


class RelatedSearch(object, metaclass=RelatedSearchMeta):
distinct = False
filters = []
Expand All @@ -76,9 +88,14 @@ class RelatedSearch(object, metaclass=RelatedSearchMeta):

@classmethod
def execute(cls, session, config, terms, collection, user, limit, offset):
queries = [_f for _f in (
cls(defn).build_related_query(session, config, terms, collection, user)
for defn in cls.definitions) if _f]
queries = [
_f
for _f in (
cls(defn).build_related_query(session, config, terms, collection, user)
for defn in cls.definitions
)
if _f
]

if len(queries) > 0:
query = queries[0].union(*queries[1:])
Expand All @@ -89,64 +106,86 @@ def execute(cls, session, config, terms, collection, user, limit, offset):
results = []

return {
'totalCount': count,
'results': results,
'definition': {
'name': cls.__name__,
'root': cls.root.name,
'link': cls.link,
'columns': cls.columns,
'fieldSpecs': [{'stringId': fs.to_stringid(), 'isRelationship': fs.is_relationship()}
for field in cls.display_fields
for fs in [field.fieldspec]]}}
"totalCount": count,
"results": results,
"definition": {
"name": cls.__name__,
"root": cls.root.name,
"link": cls.link,
"columns": cls.columns,
"fieldSpecs": [
{
"stringId": fs.to_stringid(),
"isRelationship": fs.is_relationship(),
}
for field in cls.display_fields
for fs in [field.fieldspec]
],
},
}

def __init__(self, definition):
self.definition = definition

def build_related_query(self, session, config, terms, collection, user):
logger.info('%s: building related query using definition: %s',
self.__class__.__name__, self.definition)
logger.info(
"%s: building related query using definition: %s",
self.__class__.__name__,
self.definition,
)

from .views import build_primary_query

primary_fieldspec = QueryFieldSpec.from_path(self.definition.split('.'), add_id=True)
primary_fieldspec = QueryFieldSpec.from_path(
self.definition.split("."), add_id=True
)

pivot = primary_fieldspec.table

logger.debug('pivoting on: %s', pivot)
for searchtable in config.findall('tables/searchtable'):
if searchtable.find('tableName').text == pivot.name:
logger.debug("pivoting on: %s", pivot)
for searchtable in config.findall("tables/searchtable"):
if searchtable.find("tableName").text == pivot.name:
break
else:
return None

logger.debug('using %s for primary search', searchtable.find('tableName').text)
primary_query = build_primary_query(session, searchtable, terms, collection, user, as_scalar=True)
logger.debug("using %s for primary search", searchtable.find("tableName").text)
primary_query = build_primary_query(
session, searchtable, terms, collection, user, as_scalar=True
)

if primary_query is None:
return None
logger.debug('primary query: %s', primary_query)
logger.debug("primary query: %s", primary_query)

primary_field = QueryField(
fieldspec=primary_fieldspec,
op_num=QueryOps.OPERATIONS.index('op_in'),
op_num=QueryOps.OPERATIONS.index("op_in"),
value=primary_query,
negate=False,
display=False,
format_name=None,
sort_type=0,
strict=False)
strict=False
)

logger.debug("primary queryfield: %s", primary_field)
logger.debug("display queryfields: %s", self.display_fields)
logger.debug("filter queryfields: %s", self.filter_fields)

queryfields = self.display_fields + self.filter_fields + [primary_field]

related_query, _ = build_query(session, collection, user, self.root.tableId, queryfields, implicit_or=False)
related_query, _ = build_query(
session,
collection,
user,
self.root.tableId,
queryfields,
props=BuildQueryProps(implicit_or=True),
)

if self.distinct:
related_query = related_query.distinct()

logger.debug('related query: %s', related_query)
logger.debug("related query: %s", related_query)
return related_query
11 changes: 9 additions & 2 deletions specifyweb/frontend/js_src/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
/* Make spinner buttons larger */
[type='number']:not([readonly], .no-arrows)::-webkit-outer-spin-button,
[type='number']:not([readonly], .no-arrows)::-webkit-inner-spin-button {
-webkit-appearance: inner-spin-button !important;
@apply absolute right-0 top-0 h-full w-2;
}

Expand Down Expand Up @@ -255,10 +256,16 @@
--invalid-cell: theme('colors.red.300');
--modified-cell: theme('colors.yellow.250');
--search-result: theme('colors.green.300');
@apply dark:[--invalid-cell:theme('colors.red.900')]
--updated-cell: theme('colors.cyan.200');
--deleted-cell: theme('colors.amber.500');
--matched-and-changed-cell: theme('colors.blue.200');
@apply dark:[--deleted-cell:theme('colors.amber.600')]
dark:[--invalid-cell:theme('colors.red.900')]
dark:[--matched-and-changed-cell:theme('colors.fuchsia.700')]
dark:[--modified-cell:theme('colors.yellow.900')]
dark:[--new-cell:theme('colors.indigo.900')]
dark:[--search-result:theme('colors.green.900')];
dark:[--search-result:theme('colors.green.900')]
dark:[--updated-cell:theme('colors.cyan.800')];
}

.custom-select {
Expand Down
25 changes: 23 additions & 2 deletions specifyweb/frontend/js_src/css/workbench.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@
}

/* CONTENT styles */
.wbs-form.wb-show-upload-results .wb-no-match-cell,
.wbs-form.wb-show-upload-results .wb-no-match-cell
.wbs-form.wb-show-upload-results .wb-updated-cell
.wbs-form.wb-show-upload-results .wb-deleted-cell
.wbs-form.wb-show-upload-results .wb-matched-and-changed-cell
.wbs-form.wb-focus-coordinates .wb-coordinate-cell {
@apply text-black dark:text-white;
}
Expand All @@ -54,14 +57,32 @@
.wb-no-match-cell,
.wb-modified-cell,
.htCommentCell,
.wb-search-match-cell
.wb-search-match-cell,
.wb-updated-cell,
.wb-deleted-cell,
.wb-matched-and-changed-cell
),
.wb-navigation-section {
@apply !bg-[color:var(--accent-color)];
}

/* The order here determines the priority of the states
* From the lowest till the highest */
.wbs-form:not(.wb-hide-new-cells) .wb-updated-cell,
.wb-navigation-section[data-navigation-type='updatedCells'] {
--accent-color: var(--updated-cell);
}

.wbs-form:not(.wb-hide-new-cells) .wb-deleted-cell,
.wb-navigation-section[data-navigation-type='deletedCells'] {
--accent-color: var(--deleted-cell);
}

.wbs-form:not(.wb-hide-new-cells) .wb-matched-and-changed-cell,
.wb-navigation-section[data-navigation-type='matchedAndChangedCells'] {
--accent-color: var(--matched-and-changed-cell);
}

.wbs-form:not(.wb-hide-new-cells) .wb-no-match-cell,
.wb-navigation-section[data-navigation-type='newCells'] {
--accent-color: var(--new-cell);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function ModifyDataset({
}

const createEmpty = async (name: LocalizedString) =>
createEmptyDataSet<AttachmentDataSet>('/attachment_gw/dataset/', name, {
createEmptyDataSet<AttachmentDataSet>('bulkAttachment', name, {
uploadplan: { staticPathKey: undefined },
uploaderstatus: 'main',
});
Expand Down Expand Up @@ -241,7 +241,7 @@ const getNamePromise = async () =>
date: new Date().toDateString(),
}),
undefined,
'/attachment_gw/dataset/'
'bulkAttachment'
);

function NewDataSet(): JSX.Element | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ export function PerformAttachmentTask({
uploaded: (nextIndex === currentIndex ? 0 : 1) + progress.uploaded,
}));
workRef.current.mappedFiles = workRef.current.mappedFiles.map(
(uploadble, postIndex) =>
postIndex === currentIndex ? postUpload : uploadble
(uploadable, postIndex) =>
postIndex === currentIndex ? postUpload : uploadable
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function AttachmentDatasetMeta({
return (
<DataSetMeta
dataset={dataset}
datasetUrl="/attachment_gw/dataset/"
datasetVariant='bulkAttachment'
deleteDescription={attachmentsText.deleteAttachmentDataSetDescription()}
permissionResource="/attachment_import/dataset"
onChange={(changed) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getResourceViewUrl } from '../DataModel/resource';
import type { Tables } from '../DataModel/types';
import { GenericSortedDataViewer } from '../Molecules/GenericSortedDataViewer';
import { useDragDropFiles } from '../Molecules/useDragDropFiles';
import { datasetVariants } from '../WbUtils/datasetVariants';
import type { PartialAttachmentUploadSpec } from './Import';
import { ResourceDisambiguationDialog } from './ResourceDisambiguation';
import type { PartialUploadableFileSpec } from './types';
Expand Down Expand Up @@ -230,7 +231,7 @@ function StartUploadDescription(): JSX.Element {
<li>{attachmentsText.chooseFilesToGetStarted()}</li>
<li>{attachmentsText.selectIdentifier()}</li>
</ol>
<Link.NewTab href="https://discourse.specifysoftware.org/t/batch-attachment-uploader/1374">
<Link.NewTab href={datasetVariants.bulkAttachment.documentationUrl}>
{headerText.documentation()}
</Link.NewTab>
</div>
Expand Down
Loading
Loading