Skip to content

Commit df0d024

Browse files
authored
Merge branch 'main' into issue-7436
2 parents e61442a + e7123b6 commit df0d024

File tree

101 files changed

+3477
-2313
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+3477
-2313
lines changed

.env

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,25 @@ DATABASE_NAME=specify
88
# See documenation https://discourse.specifysoftware.org/t/new-blank-database-creation-database-user-levels/3023
99

1010
# MASTER Database User
11-
# Full database administrator, used for initial setup and migrations requiring elevated privileges.
11+
# Full database administrator, used for initial setup and migrations requiring elevated privileges.
12+
# This user should already be setup before running Specify.
1213
MASTER_NAME=root
1314
MASTER_PASSWORD=password
15+
MASTER_HOST=%
1416

1517
# MIGRATOR Database User
1618
# User with elevated privileges to perform migrations (create/drop/modify tables, etc.), for Django migration steps.
19+
# Make sure that the user is unique to just one database, otherwise use master.
1720
MIGRATOR_NAME=specify_migrator
1821
MIGRATOR_PASSWORD=specify_migrator
22+
MIGRATOR_HOST=%
1923

2024
# APP Database User
2125
# Normal runtime database user that performs application-level operations.
26+
# Make sure that the user is unique to just one database, otherwise use master.
2227
APP_USER_NAME=specify_user
2328
APP_USER_PASSWORD=specify_user
29+
APP_HOST=%
2430

2531
# Enabling this option allows administrators with access to the
2632
# backend Specify instance to log in as any user for support

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ wheel
55
kombu==5.5.2
66
redis==6.4.0
77
celery==5.5.1
8-
Django==4.2.24
8+
Django==4.2.27
99
mysqlclient==2.1.1
1010
SQLAlchemy==1.4.54
1111
sqlalchemy2-stubs

sp7_db_setup_check.sh

Lines changed: 318 additions & 141 deletions
Large diffs are not rendered by default.

specifyweb/backend/context/data/viewset_5.xml

Lines changed: 0 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -553,128 +553,6 @@
553553
<row>
554554
<cell colspan="3" id="-20" label="PROMPT_TO_REPLACE_SYNONYM" name="Determination.PromptToReplaceSynonym" type="field" uitype="checkbox" />
555555
</row>
556-
<row>
557-
<cell coldef="p,2px,p,p:g,f:p:g" id="16tx" name="treepaneltx" rowdef="p,2px,p,2px,p,2px,p,2px,p" type="panel">
558-
<rows>
559-
<row>
560-
<cell colspan="5" label="TAXON_TREE" type="separator" />
561-
</row>
562-
<row>
563-
<cell label="TT_SHOW_COUNTS_BELOW" labelfor="1" type="label" />
564-
<cell colspan="3" id="1" ignore="true" name="TreeEditor.Rank.Threshold.Taxon" type="field" uitype="combobox" />
565-
</row>
566-
<row>
567-
<cell label="TT_COL_CLR_1" labelfor="TreeColColor1.Taxon" type="label" />
568-
<cell id="TreeColColor1.Taxon" name="Treeeditor.TreeColColor1.Taxon" type="field" uitype="colorchooser" />
569-
</row>
570-
<row>
571-
<cell label="TT_COL_CLR_2" labelfor="TreeColColor2.Taxon" type="label" />
572-
<cell id="TreeColColor2.Taxon" name="Treeeditor.TreeColColor2.Taxon" type="field" uitype="colorchooser" />
573-
</row>
574-
<row>
575-
<cell label="TT_SYN_CLR" labelfor="SynonymyColor.Taxon" type="label" />
576-
<cell id="SynonymyColor.Taxon" name="Treeeditor.SynonymyColor.Taxon" type="field" uitype="colorchooser" />
577-
</row>
578-
</rows>
579-
</cell>
580-
<cell coldef="p,2px,p,p:g,f:p:g" id="16gt" name="treepanelgt" rowdef="p,2px,p,2px,p,2px,p,2px,p" type="panel">
581-
<rows>
582-
<row>
583-
<cell colspan="5" label="GEO_TREE" type="separator" />
584-
</row>
585-
<row>
586-
<cell label="GT_SHOW_COUNTS_BELOW" labelfor="2" type="label" />
587-
<cell colspan="3" id="2" ignore="true" name="TreeEditor.Rank.Threshold.Geography" type="field" uitype="combobox" />
588-
</row>
589-
<row>
590-
<cell label="GT_COL_CLR_1" labelfor="TreeColColor1.Geography" type="label" />
591-
<cell id="TreeColColor1.Geography" name="Treeeditor.TreeColColor1.Geography" type="field" uitype="colorchooser" />
592-
</row>
593-
<row>
594-
<cell label="GT_COL_CLR_2" labelfor="TreeColColor2.Geography" type="label" />
595-
<cell id="TreeColColor2.Geography" name="Treeeditor.TreeColColor2.Geography" type="field" uitype="colorchooser" />
596-
</row>
597-
<row>
598-
<cell label="GT_SYN_CLR" labelfor="SynonymyColor.Geography" type="label" />
599-
<cell id="SynonymyColor.Geography" name="Treeeditor.SynonymyColor.Geography" type="field" uitype="colorchooser" />
600-
</row>
601-
</rows>
602-
</cell>
603-
</row>
604-
605-
<row>
606-
<cell coldef="p,2px,p,p:g,f:p:g" id="16lt" name="treepanellt" rowdef="p,2px,p,2px,p,2px,p,2px,p" type="panel">
607-
<rows>
608-
<row>
609-
<cell colspan="5" label="LITHO_TREE" type="separator" />
610-
</row>
611-
<row>
612-
<cell label="LT_SHOW_COUNTS_BELOW" labelfor="3" type="label" />
613-
<cell colspan="3" id="3" ignore="true" name="TreeEditor.Rank.Threshold.LithoStrat" type="field" uitype="combobox" />
614-
</row>
615-
<row>
616-
<cell label="LT_COL_CLR_1" labelfor="TreeColColor1.LithoStrat" type="label" />
617-
<cell id="TreeColColor1.LithoStrat" name="Treeeditor.TreeColColor1.LithoStrat" type="field" uitype="colorchooser" />
618-
</row>
619-
<row>
620-
<cell label="LT_COL_CLR_2" labelfor="TreeColColor2.LithoStrat" type="label" />
621-
<cell id="TreeColColor2.LithoStrat" name="Treeeditor.TreeColColor2.LithoStrat" type="field" uitype="colorchooser" />
622-
</row>
623-
<row>
624-
<cell label="LT_SYN_CLR" labelfor="SynonymyColor.LithoStrat" type="label" />
625-
<cell id="SynonymyColor.LithoStrat" name="Treeeditor.SynonymyColor.LithoStrat" type="field" uitype="colorchooser" />
626-
</row>
627-
</rows>
628-
</cell>
629-
<cell coldef="p,2px,p,p:g,f:p:g" id="16geo" name="treepanelgeo" rowdef="p,2px,p,2px,p,2px,p,2px,p" type="panel">
630-
<rows>
631-
<row>
632-
<cell colspan="5" label="GEO_PERIOD_TREE" type="separator" />
633-
</row>
634-
<row>
635-
<cell label="GP_SHOW_COUNTS_BELOW" labelfor="4" type="label" />
636-
<cell colspan="3" id="4" ignore="true" name="TreeEditor.Rank.Threshold.GeologicTimePeriod" type="field" uitype="combobox" />
637-
</row>
638-
<row>
639-
<cell label="GP_COL_CLR_1" labelfor="TreeColColor1.GeologicTimePeriod" type="label" />
640-
<cell id="TreeColColor1.GeologicTimePeriod" name="Treeeditor.TreeColColor1.GeologicTimePeriod" type="field" uitype="colorchooser" />
641-
</row>
642-
<row>
643-
<cell label="GP_COL_CLR_2" labelfor="TreeColColor2.GeologicTimePeriod" type="label" />
644-
<cell id="TreeColColor2.GeologicTimePeriod" name="Treeeditor.TreeColColor2.GeologicTimePeriod" type="field" uitype="colorchooser" />
645-
</row>
646-
<row>
647-
<cell label="GP_SYN_CLR" labelfor="SynonymyColor.GeologicTimePeriod" type="label" />
648-
<cell id="SynonymyColor.GeologicTimePeriod" name="Treeeditor.SynonymyColor.GeologicTimePeriod" type="field" uitype="colorchooser" />
649-
</row>
650-
</rows>
651-
</cell>
652-
</row>
653-
<row>
654-
<cell coldef="p,2px,p,p:g,f:p:g" id="16st" name="treepanelst" rowdef="p,2px,p,2px,p,2px,p,2px,p" type="panel">
655-
<rows>
656-
<row>
657-
<cell colspan="5" label="STORAGE_TREE" type="separator" />
658-
</row>
659-
<row>
660-
<cell label="ST_SHOW_COUNTS_BELOW" labelfor="5" type="label" />
661-
<cell colspan="3" id="5" ignore="true" name="TreeEditor.Rank.Threshold.Storage" type="field" uitype="combobox" />
662-
</row>
663-
<row>
664-
<cell label="ST_COL_CLR_1" labelfor="TreeColColor1.Storage" type="label" />
665-
<cell id="TreeColColor1.Storage" name="Treeeditor.TreeColColor1.Storage" type="field" uitype="colorchooser" />
666-
</row>
667-
<row>
668-
<cell label="ST_COL_CLR_2" labelfor="TreeColColor2.Storage" type="label" />
669-
<cell id="TreeColColor2.Storage" name="Treeeditor.TreeColColor2.Storage" type="field" uitype="colorchooser" />
670-
</row>
671-
<row>
672-
<cell label="ST_SYN_CLR" labelfor="SynonymyColor.Storage" type="label" />
673-
<cell id="SynonymyColor.Storage" name="Treeeditor.SynonymyColor.Storage" type="field" uitype="colorchooser" />
674-
</row>
675-
</rows>
676-
</cell>
677-
</row>
678556
</rows>
679557
</viewdef>
680558

@@ -1202,10 +1080,6 @@
12021080
<cell label="Name" labelfor="name" type="label" />
12031081
<cell id="name" isrequired="true" name="name" type="field" uitype="text" />
12041082
</row>
1205-
<row>
1206-
<cell label="Display Columns" labelfor="displayColumns" type="label" />
1207-
<cell colspan="4" id="displayColumns" isrequired="true" name="displayColumns" type="field" uitype="textarea" />
1208-
</row>
12091083
<row>
12101084
<cell label="Search Field Name" labelfor="searchFieldName" type="label" />
12111085
<cell colspan="4" id="searchFieldName" isrequired="true" name="searchFieldName" type="field" uitype="text" />
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from django.db import migrations
2+
from django.db.models import Count
3+
4+
def deduplicate_picklists(apps, schema_editor):
5+
Picklist = apps.get_model('specify', 'Picklist')
6+
PicklistItem = apps.get_model('specify', 'PicklistItem')
7+
8+
duplicate_groups = (
9+
Picklist.objects
10+
.values('name', 'tablename', 'fieldname', 'collection')
11+
.annotate(pl_count=Count('id'))
12+
.filter(pl_count__gt=1)
13+
)
14+
15+
for group in duplicate_groups:
16+
picklists = (
17+
Picklist.objects
18+
.filter(
19+
name=group['name'],
20+
tablename=group['tablename'],
21+
fieldname=group['fieldname'],
22+
collection=group['collection'],
23+
)
24+
.order_by('id')
25+
)
26+
27+
if picklists.count() < 2:
28+
continue
29+
30+
primary = picklists.first()
31+
duplicates = picklists.exclude(id=primary.id)
32+
33+
existing_pairs = set(
34+
PicklistItem.objects
35+
.filter(picklist=primary)
36+
.values_list('title', 'value')
37+
)
38+
39+
for dup in duplicates:
40+
dup_items = list(
41+
PicklistItem.objects
42+
.filter(picklist=dup)
43+
.only('id', 'title', 'value', 'picklist')
44+
.order_by('id')
45+
)
46+
47+
# Partition into items to be either move or delete
48+
to_move = []
49+
to_delete_ids = []
50+
for it in dup_items:
51+
key = (it.title, it.value)
52+
if key in existing_pairs:
53+
to_delete_ids.append(it.id)
54+
else:
55+
it.picklist = primary
56+
to_move.append(it)
57+
existing_pairs.add(key)
58+
59+
if to_move:
60+
PicklistItem.objects.bulk_update(to_move, ['picklist'])
61+
if to_delete_ids:
62+
PicklistItem.objects.filter(id__in=to_delete_ids).delete()
63+
64+
# Remove the empty duplicate picklist
65+
dup.delete()
66+
67+
class Migration(migrations.Migration):
68+
dependencies = [
69+
('patches', '0007_fix_tectonicunit_tree_root'),
70+
]
71+
72+
operations = [
73+
migrations.RunPython(
74+
deduplicate_picklists,
75+
reverse_code=migrations.RunPython.noop,
76+
atomic=True,
77+
),
78+
]

specifyweb/backend/stored_queries/format.py

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -200,32 +200,57 @@ def make_expr(self,
200200
formatter,
201201
aggregator, previous_tables)
202202

203+
raw_expr = new_expr
203204
else:
204205
new_query, table, model, specify_field = query.build_join(
205206
specify_model, orm_table, formatter_field_spec.join_path)
206207
new_expr = getattr(table, specify_field.name)
207-
208+
raw_expr = new_expr
209+
210+
# Apply field-level formatting, which may include numeric cast
208211
if self.format_expr:
209-
new_expr = self._fieldformat(formatter_field_spec.table, formatter_field_spec.get_field(), new_expr)
210-
211-
if 'trimzeros' in fieldNodeAttrib:
212-
# new_expr = case(
213-
# [(new_expr.op('REGEXP')('^-?[0-9]+(\\.[0-9]+)?$'), cast(new_expr, types.Numeric(65)))],
214-
# else_=new_expr
215-
# )
216-
numeric_str = cast(cast(new_expr, types.Numeric(65)), types.String())
217-
new_expr = case(
218-
(new_expr.op('REGEXP')('^-?[0-9]+(\\.[0-9]+)?$'), numeric_str),
219-
else_=cast(new_expr, types.String()),
212+
new_expr = self._fieldformat(
213+
formatter_field_spec.table,
214+
formatter_field_spec.get_field(),
215+
new_expr
216+
)
217+
218+
# Helper function to apply only string-ish transforms with no numeric casts
219+
def apply_stringish(expr):
220+
e = expr
221+
if fieldNodeAttrib.get('trimzeros') == 'true':
222+
numeric_str = cast(cast(e, types.Numeric(65)), types.String())
223+
e = case(
224+
(e.op('REGEXP')(r'^-?[0-9]+(\.[0-9]+)?$'), numeric_str),
225+
else_=cast(e, types.String()),
226+
)
227+
fmt = fieldNodeAttrib.get('format')
228+
if fmt is not None:
229+
e = self.pseudo_sprintf(fmt, e)
230+
sep = fieldNodeAttrib.get('sep')
231+
if sep is not None:
232+
e = concat(sep, e)
233+
return e
234+
235+
stringish_expr = apply_stringish(raw_expr)
236+
formatted_expr = apply_stringish(new_expr)
237+
238+
if do_blank_null:
239+
sf = formatter_field_spec.get_field()
240+
is_catalog_num = (
241+
sf is not None
242+
and sf is CollectionObject_model.get_field('catalogNumber')
220243
)
244+
if (
245+
is_catalog_num
246+
and self.numeric_catalog_number
247+
and all_numeric_catnum_formats(self.collection)
248+
):
249+
return new_query, blank_nulls(stringish_expr), formatter_field_spec
221250

222-
if 'format' in fieldNodeAttrib:
223-
new_expr = self.pseudo_sprintf(fieldNodeAttrib['format'], new_expr)
224-
225-
if 'sep' in fieldNodeAttrib:
226-
new_expr = concat(fieldNodeAttrib['sep'], new_expr)
251+
return new_query, blank_nulls(formatted_expr), formatter_field_spec
227252

228-
return new_query, blank_nulls(new_expr) if do_blank_null else new_expr, formatter_field_spec
253+
return new_query, formatted_expr, formatter_field_spec
229254

230255
def objformat(self, query: QueryConstruct, orm_table: SQLTable,
231256
formatter_name, cycle_detector=[]) -> tuple[QueryConstruct, blank_nulls]:
@@ -442,11 +467,15 @@ def _fieldformat(self, table: Table, specify_field: Field,
442467

443468
if self.numeric_catalog_number and specify_field is CollectionObject_model.get_field('catalogNumber') \
444469
and all_numeric_catnum_formats(self.collection):
470+
# Cast to Numeric only if the value is numeric to avoid casting strings like 'F-235694' to 0
445471
# While the frontend can format the catalogNumber if needed,
446472
# processes like reports, labels, and query exports generally
447473
# expect the catalogNumber to be numeric if possible.
448474
# See https://github.com/specify/specify7/issues/6464
449-
return cast(field, types.Numeric(65))
475+
return case(
476+
[(field.op('REGEXP')(r'^-?[0-9]+(\.[0-9]+)?$'), cast(field, types.Numeric(65)))],
477+
else_=field
478+
)
450479

451480
if self.format_types and specify_field.type == 'json' and isinstance(field.comparator.type, types.JSON):
452481
return cast(field, types.Text)

0 commit comments

Comments
 (0)