From 8984625cbb23ebe6e708849a418bd90be556e3c8 Mon Sep 17 00:00:00 2001 From: realVinayak Date: Thu, 4 Jul 2024 20:31:34 -0500 Subject: [PATCH 01/27] Use a backend solution for synonym concat --- .../js_src/lib/components/TreeView/Row.tsx | 21 +++---------------- .../js_src/lib/components/TreeView/helpers.ts | 5 ++++- specifyweb/specify/tree_views.py | 21 ++++++++++++++----- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx index 2e4e53eadf2..f3cd629f9ed 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx @@ -150,21 +150,6 @@ export function TreeRow({ const hasNoChildrenNodes = nodeStats?.directCount === 0 && nodeStats.childCount === 0; - const acceptedChildrenKey = `accepted${treeName.toLowerCase()}`; - const [synonymsNames] = useAsyncState( - React.useCallback( - async () => - fetchRows(treeName as 'Taxon', { - fields: { name: ['string'] }, - limit: 0, - [acceptedChildrenKey]: row.nodeId, - domainFilter: false, - }).then((rows) => rows.map(({ name }) => name)), - [acceptedChildrenKey, treeName, row.nodeId] - ), - false - ); - return hideEmptyNodes && hasNoChildrenNodes ? null : (
  • {ranks.map((rankId) => { @@ -247,11 +232,11 @@ export function TreeRow({ ? treeText.acceptedName({ name: row.acceptedName ?? row.acceptedId.toString(), }) - : synonymsNames === undefined || - synonymsNames.length === 0 + // backend will never return undefined... + : row.synonymConcat === null ? undefined : treeText.synonyms({ - names: synonymsNames.join(', '), + names: row.synonymConcat, }) } > diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts b/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts index f1404be9d4d..7a57cebf519 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts +++ b/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts @@ -25,7 +25,8 @@ export const fetchRows = async (fetchUrl: string) => number | null, string | null, string, - number + number, + string ] > >(fetchUrl, { @@ -44,6 +45,7 @@ export const fetchRows = async (fetchUrl: string) => acceptedName = undefined, author = undefined, children, + synonymConcat ], index, { length } @@ -59,6 +61,7 @@ export const fetchRows = async (fetchUrl: string) => author, children, isLastChild: index + 1 === length, + synonymConcat }) ) ); diff --git a/specifyweb/specify/tree_views.py b/specifyweb/specify/tree_views.py index 9bf78957513..ec686fef3d4 100644 --- a/specifyweb/specify/tree_views.py +++ b/specifyweb/specify/tree_views.py @@ -3,7 +3,7 @@ from django.db import transaction from django.http import HttpResponse from django.views.decorators.http import require_POST -from sqlalchemy import sql +from sqlalchemy import sql, distinct from sqlalchemy.orm import aliased from specifyweb.middleware.general import require_GET @@ -12,6 +12,8 @@ PermissionTargetAction, check_permission_targets from specifyweb.specify.tree_ranks import tree_rank_count from specifyweb.stored_queries import models +from specifyweb.stored_queries.execution import set_group_concat_max_len +from specifyweb.stored_queries.group_concat import group_concat from . import tree_extras from .api import get_object_or_404, obj_to_data, toJson from .auditcodes import TREE_MOVE @@ -110,7 +112,10 @@ def wrapper(*args, **kwargs): "type" : "integer", "description" : "The number of children the child node has" - + }, + { + "type": "string", + "description": "concat of fullname of syns" } ], } @@ -134,6 +139,7 @@ def tree_view(request, treedef, tree, parentid, sortfield): node = getattr(models, tree_table.name) child = aliased(node) accepted = aliased(node) + synonym = aliased(node) id_col = getattr(node, node._id) child_id = getattr(child, node._id) treedef_col = getattr(node, tree_table.name + "TreeDefID") @@ -150,6 +156,7 @@ def tree_view(request, treedef, tree, parentid, sortfield): 'includeauthor') if 'includeauthor' in request.GET else False with models.session_context() as session: + set_group_concat_max_len(session) query = session.query(id_col, node.name, node.fullName, @@ -158,11 +165,15 @@ def tree_view(request, treedef, tree, parentid, sortfield): node.rankId, node.AcceptedID, accepted.fullName, - node.author if ( - includeAuthor and tree == 'taxon') else "NULL", - sql.functions.count(child_id)) \ + node.author if (includeAuthor and tree == 'taxon') else "NULL", + # syns are to-many, so child can be duplicated + sql.func.count(distinct(child_id)), + # child are to-many, so syn's full name can be duplicated + # FEATURE: Allow users to select a separator?? Maybe that's too nice + group_concat(distinct(synonym.fullName), separator=', ')) \ .outerjoin(child, child.ParentID == id_col) \ .outerjoin(accepted, node.AcceptedID == getattr(accepted, node._id)) \ + .outerjoin(synonym, synonym.AcceptedID == id_col) \ .group_by(id_col) \ .filter(treedef_col == int(treedef)) \ .filter(node.ParentID == parentid) \ From f644325a978c4e7803b3843a79918f7a54befffc Mon Sep 17 00:00:00 2001 From: realVinayak Date: Thu, 4 Jul 2024 20:55:54 -0500 Subject: [PATCH 02/27] Patch only_full_group_by causing issues in some db settings --- specifyweb/specify/tree_views.py | 47 ++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/specifyweb/specify/tree_views.py b/specifyweb/specify/tree_views.py index ec686fef3d4..272017597e0 100644 --- a/specifyweb/specify/tree_views.py +++ b/specifyweb/specify/tree_views.py @@ -157,20 +157,39 @@ def tree_view(request, treedef, tree, parentid, sortfield): with models.session_context() as session: set_group_concat_max_len(session) - query = session.query(id_col, - node.name, - node.fullName, - node.nodeNumber, - node.highestChildNodeNumber, - node.rankId, - node.AcceptedID, - accepted.fullName, - node.author if (includeAuthor and tree == 'taxon') else "NULL", - # syns are to-many, so child can be duplicated - sql.func.count(distinct(child_id)), - # child are to-many, so syn's full name can be duplicated - # FEATURE: Allow users to select a separator?? Maybe that's too nice - group_concat(distinct(synonym.fullName), separator=', ')) \ + + col_args = [ + node.name, + node.fullName, + node.nodeNumber, + node.highestChildNodeNumber, + node.rankId, + node.AcceptedID, + accepted.fullName, + node.author if (includeAuthor and tree == 'taxon') else "NULL", + ] + + apply_min = [ + # for some reason, SQL is rejecting the group_by in some dbs + # due to "only_full_group_by". It is somehow not smart enough to see + # that there is no dependency in the columns going from main table to the to-manys (child, and syns) + # I want to use ANY_VALUE() but that's not supported by MySQL 5.6- and MariaDB. + # I don't want to disable "only_full_group_by" in case someone misuses it... + # applying min to fool into thinking it is aggregated. + # these values are guarenteed to be the same + sql.func.min(arg) for arg in col_args + ] + + grouped = [ + *apply_min, + # syns are to-many, so child can be duplicated + sql.func.count(distinct(child_id)), + # child are to-many, so syn's full name can be duplicated + # FEATURE: Allow users to select a separator?? Maybe that's too nice + group_concat(distinct(synonym.fullName), separator=', ') + ] + + query = session.query(id_col, *grouped) \ .outerjoin(child, child.ParentID == id_col) \ .outerjoin(accepted, node.AcceptedID == getattr(accepted, node._id)) \ .outerjoin(synonym, synonym.AcceptedID == id_col) \ From 2bf112b9717c2dae42179dd8edf1ef0cd82a4df4 Mon Sep 17 00:00:00 2001 From: realVinayak Date: Thu, 4 Jul 2024 20:57:58 -0500 Subject: [PATCH 03/27] Remove unused deps --- specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx index f3cd629f9ed..1edae90e597 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { useAsyncState } from '../../hooks/useAsyncState'; import { useId } from '../../hooks/useId'; import { commonText } from '../../localization/common'; import { treeText } from '../../localization/tree'; @@ -8,7 +7,6 @@ import type { RA } from '../../utils/types'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; import { icons } from '../Atoms/Icons'; -import { fetchRows } from '../DataModel/collection'; import type { AnyTree } from '../DataModel/helperTypes'; import { getPref } from '../InitialContext/remotePrefs'; import { userPreferences } from '../Preferences/userPreferences'; From 245b6396b7118d8ae5fa7fb1dfc4de4bc236e31c Mon Sep 17 00:00:00 2001 From: realVinayak Date: Fri, 5 Jul 2024 02:01:23 +0000 Subject: [PATCH 04/27] Lint code with ESLint and Prettier Triggered by 8644aea3222dc16e2e24e03bdcf9aa59cd79c48f on branch refs/heads/issue-5067 --- specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx index 1edae90e597..38c1548fbbb 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx @@ -230,8 +230,8 @@ export function TreeRow({ ? treeText.acceptedName({ name: row.acceptedName ?? row.acceptedId.toString(), }) - // backend will never return undefined... - : row.synonymConcat === null + : // Backend will never return undefined... + row.synonymConcat === null ? undefined : treeText.synonyms({ names: row.synonymConcat, From e89f02fe9550fba03322612986cde36bbada97aa Mon Sep 17 00:00:00 2001 From: realVinayak Date: Thu, 4 Jul 2024 21:26:37 -0500 Subject: [PATCH 05/27] Optimize tree stats more -- see the comment in tree_stats.py --- specifyweb/specify/tree_stats.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/specifyweb/specify/tree_stats.py b/specifyweb/specify/tree_stats.py index 2ff038cf9d7..73f4a7e7586 100644 --- a/specifyweb/specify/tree_stats.py +++ b/specifyweb/specify/tree_stats.py @@ -12,6 +12,7 @@ def get_tree_stats(treedef, tree, parentid, specify_collection, session_context, using_cte): tree_table = datamodel.get_table(tree) + tree_def_item = getattr(models, tree_table.name + 'TreeDefItem') parentid = None if parentid == 'null' else int(parentid) treedef_col = tree_table.name + "TreeDefID" @@ -62,19 +63,20 @@ def wrap_cte_query(cte_query, query): results = None - with session_context() as session: # The join depth only needs to be enough to reach the bottom of the tree. - # That will be the number of distinct rankID values not less than - # the rankIDs of the children of parentid. - - # Also used in Recursive CTE to make sure cycles don't cause crash (very rare) - - highest_rank = session.query(sql.func.min(tree_node.rankId)).filter( - tree_node.ParentID == parentid).as_scalar() - depth, = \ - session.query(sql.func.count(distinct(tree_node.rankId))).filter( - tree_node.rankId >= highest_rank)[0] + # "correct" depth is depth based on actual tree + # "incorrect" depth > "correct" is naively based on item-table + # If we use "correct" depth, we will make less joins in CTE (so CTE will be faster) + # but, "correct" depth takes too long to compute (needs to look at main tree table) + # As a compromise, we look at defitem table for "incorrect" depth, will be higher than "correct" + # depth. So yes, technicallly CTE will take "more" time, but experimentation reveals that + # CTE's "more" time, is still very much low than time taken to compute "correct" depth. + # I don't even want to use depth, but some pathological tree might have cycles, and CTE depth + # might be in millions as a custom setting.. + + depth = session.query(sql.func.count(getattr(tree_def_item, tree_def_item._id))).filter( + getattr(tree_def_item, treedef_col) == int(treedef)).as_scalar() query = None try: if using_cte: From d20d74c58e224dc019557ea29c6f5b38cbf7f464 Mon Sep 17 00:00:00 2001 From: realVinayak Date: Thu, 4 Jul 2024 21:51:45 -0500 Subject: [PATCH 06/27] Force early depth evaluation --- specifyweb/specify/tree_stats.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specifyweb/specify/tree_stats.py b/specifyweb/specify/tree_stats.py index 73f4a7e7586..30d6cca9a97 100644 --- a/specifyweb/specify/tree_stats.py +++ b/specifyweb/specify/tree_stats.py @@ -75,8 +75,9 @@ def wrap_cte_query(cte_query, query): # I don't even want to use depth, but some pathological tree might have cycles, and CTE depth # might be in millions as a custom setting.. - depth = session.query(sql.func.count(getattr(tree_def_item, tree_def_item._id))).filter( - getattr(tree_def_item, treedef_col) == int(treedef)).as_scalar() + depth_query = session.query(sql.func.count(getattr(tree_def_item, tree_def_item._id))).filter( + getattr(tree_def_item, treedef_col) == int(treedef)) + depth, = list(depth_query)[0] query = None try: if using_cte: From ff8f5b5c41f1cacfe73c7dcdb59d91848f030f5d Mon Sep 17 00:00:00 2001 From: realVinayak Date: Wed, 17 Jul 2024 12:10:45 -0500 Subject: [PATCH 07/27] Add unit tests + minor refactors --- .../js_src/lib/components/TreeView/Row.tsx | 8 +- .../js_src/lib/components/TreeView/helpers.ts | 4 +- specifyweb/specify/tests/test_trees.py | 181 +++++++++--------- specifyweb/specify/tree_extras.py | 2 +- specifyweb/specify/tree_views.py | 111 +++++------ specifyweb/stored_queries/execution.py | 10 +- specifyweb/stored_queries/tests.py | 4 +- 7 files changed, 164 insertions(+), 156 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx index 38c1548fbbb..0b6c38bf58c 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx @@ -231,11 +231,11 @@ export function TreeRow({ name: row.acceptedName ?? row.acceptedId.toString(), }) : // Backend will never return undefined... - row.synonymConcat === null - ? undefined - : treeText.synonyms({ - names: row.synonymConcat, + typeof row.synonyms === 'string' + ? treeText.synonyms({ + names: row.synonyms, }) + : undefined } > {doIncludeAuthorPref && diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts b/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts index 7a57cebf519..544cce3fdfb 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts +++ b/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts @@ -45,7 +45,7 @@ export const fetchRows = async (fetchUrl: string) => acceptedName = undefined, author = undefined, children, - synonymConcat + synonyms = undefined ], index, { length } @@ -61,7 +61,7 @@ export const fetchRows = async (fetchUrl: string) => author, children, isLastChild: index + 1 === length, - synonymConcat + synonyms }) ) ); diff --git a/specifyweb/specify/tests/test_trees.py b/specifyweb/specify/tests/test_trees.py index 289379294e3..598c53e83c1 100644 --- a/specifyweb/specify/tests/test_trees.py +++ b/specifyweb/specify/tests/test_trees.py @@ -4,7 +4,11 @@ from specifyweb.specify.tests.test_api import ApiTests, get_table from specifyweb.specify.tree_stats import get_tree_stats from specifyweb.specify.tree_extras import set_fullnames +from specifyweb.specify.tree_views import get_tree_rows +from specifyweb.stored_queries.execution import set_group_concat_max_len from specifyweb.stored_queries.tests import SQLAlchemySetup +from contextlib import contextmanager +from django.db import connection class TestTreeSetup(ApiTests): def setUp(self) -> None: @@ -30,103 +34,42 @@ def setUp(self) -> None: self.taxontreedef.treedefitems.create(name='Subspecies', rankid=230) class TestTree: + def setUp(self)->None: super().setUp() - self.earth = get_table('Geography').objects.create( - name="Earth", - definitionitem=get_table('Geographytreedefitem').objects.get(name="Planet"), - definition=self.geographytreedef, - ) + + self.earth = self.make_geotree("Earth", "Planet") - self.na = get_table('Geography').objects.create( - name="North America", - definitionitem=get_table('Geographytreedefitem').objects.get(name="Continent"), - definition=self.geographytreedef, - parent=self.earth, - ) + self.na = self.make_geotree("North America", "Continent", parent=self.earth) - self.usa = get_table('Geography').objects.create( - name="USA", - definitionitem=get_table('Geographytreedefitem').objects.get(name="Country"), - definition=self.geographytreedef, - parent=self.na, - ) - - self.kansas = get_table('Geography').objects.create( - name="Kansas", - definitionitem=get_table('Geographytreedefitem').objects.get(name="State"), - definition=self.geographytreedef, - parent=self.usa, - ) + self.usa = self.make_geotree("USA", "Country", parent=self.na) - self.mo = get_table('Geography').objects.create( - name="Missouri", - definitionitem=get_table('Geographytreedefitem').objects.get(name="State"), - definition=self.geographytreedef, - parent=self.usa, - ) + self.kansas = self.make_geotree("Kansas", "State", parent=self.usa) + self.mo = self.make_geotree("Missouri", "State", parent=self.usa) + self.ohio = self.make_geotree("Ohio", "State", parent=self.usa) + self.ill = self.make_geotree("Illinois", "State", parent=self.usa) - self.ohio = get_table('Geography').objects.create( - name="Ohio", - definitionitem=get_table('Geographytreedefitem').objects.get(name="State"), - definition=self.geographytreedef, - parent=self.usa, - ) + self.doug = self.make_geotree("Douglas", "County", parent=self.kansas) + self.greene = self.make_geotree("Greene", "County", parent=self.mo) + self.greeneoh = self.make_geotree("Greene", "County", parent=self.ohio) + self.sangomon = self.make_geotree("Sangamon", "County", parent=self.ill) - self.ill = get_table('Geography').objects.create( - name="Illinois", - definitionitem=get_table('Geographytreedefitem').objects.get(name="State"), - definition=self.geographytreedef, - parent=self.usa, - ) + self.springmo = self.make_geotree("Springfield", "City", parent=self.greene) + self.springill = self.make_geotree("Springfield", "City", parent=self.sangomon) - self.doug = get_table('Geography').objects.create( - name="Douglas", - definitionitem=get_table('Geographytreedefitem').objects.get(name="County"), + def make_geotree(self, name, rank_name, **extra_kwargs): + return get_table("Geography").objects.create( + name=name, + definitionitem=get_table('Geographytreedefitem').objects.get(name=rank_name), definition=self.geographytreedef, - parent=self.kansas, + **extra_kwargs ) - - self.greene = get_table('Geography').objects.create( - name="Greene", - definitionitem=get_table('Geographytreedefitem').objects.get(name="County"), - definition=self.geographytreedef, - parent=self.mo, - ) - - self.greeneoh = get_table('Geography').objects.create( - name="Greene", - definitionitem=get_table('Geographytreedefitem').objects.get(name="County"), - definition=self.geographytreedef, - parent=self.ohio, - ) - - self.sangomon = get_table('Geography').objects.create( - name="Sangamon", - definitionitem=get_table('Geographytreedefitem').objects.get(name="County"), - definition=self.geographytreedef, - parent=self.ill, - ) - - self.springmo = get_table('Geography').objects.create( - name="Springfield", - definitionitem=get_table('Geographytreedefitem').objects.get(name="City"), - definition=self.geographytreedef, - parent=self.greene, - ) - - self.springill = get_table('Geography').objects.create( - name="Springfield", - definitionitem=get_table('Geographytreedefitem').objects.get(name="City"), - definition=self.geographytreedef, - parent=self.sangomon, - ) - + class GeographyTree(TestTree, TestTreeSetup): pass class SqlTreeSetup(SQLAlchemySetup, GeographyTree): pass -class TreeStatsTest(SqlTreeSetup): +class TreeViewsTest(SqlTreeSetup): def setUp(self): super().setUp() @@ -180,13 +123,14 @@ def setUp(self): ) def _run_nn_and_cte(*args, **kwargs): - cte_results = get_tree_stats(*args, **kwargs, session_context=TreeStatsTest.test_session_context, using_cte=True) - node_number_results = get_tree_stats(*args, **kwargs, session_context=TreeStatsTest.test_session_context, using_cte=False) + cte_results = get_tree_stats(*args, **kwargs, session_context=TreeViewsTest.test_session_context, using_cte=True) + node_number_results = get_tree_stats(*args, **kwargs, session_context=TreeViewsTest.test_session_context, using_cte=False) self.assertCountEqual(cte_results, node_number_results) return cte_results self.validate_tree_stats = lambda *args, **kwargs: ( lambda true_results: self.assertCountEqual(_run_nn_and_cte(*args, **kwargs), true_results)) + def test_counts_correctness(self): correct_results = { @@ -204,12 +148,77 @@ def test_counts_correctness(self): self.sangomon.id: [ (self.springill.id, 1, 1) ] - } + } _results = [ self.validate_tree_stats(self.geographytreedef.id, 'geography', parent_id, self.collection)(correct) for parent_id, correct in correct_results.items() ] + + def test_test_synonyms_concat(self): + self.maxDiff = None + na_syn_0 = self.make_geotree("NA Syn 0", "Continent", + acceptedgeography=self.na, + # fullname is not set by default for not-accepted + fullname="NA Syn 0", + parent=self.earth + ) + na_syn_1 = self.make_geotree("NA Syn 1", "Continent", acceptedgeography=self.na, fullname="NA Syn 1", parent=self.earth) + + usa_syn_0 = self.make_geotree("USA Syn 0", "Country", acceptedgeography=self.usa, parent=self.na, fullname="USA Syn 0") + usa_syn_1 = self.make_geotree("USA Syn 1", "Country", acceptedgeography=self.usa, parent=self.na, fullname="USA Syn 1") + usa_syn_2 = self.make_geotree("USA Syn 2", "Country", acceptedgeography=self.usa, parent=self.na, fullname="USA Syn 2") + + # need to refresh _some_ nodes (but not all) + # just the immediate parents and siblings inserted before us + # to be safe, we could refresh all, but I'm not going to, so that bug in those sections can be caught here + self.earth.refresh_from_db() + self.na.refresh_from_db() + self.usa.refresh_from_db() + + na_syn_0.refresh_from_db() + na_syn_1.refresh_from_db() + + usa_syn_1.refresh_from_db() + usa_syn_0.refresh_from_db() + + @contextmanager + def _run_for_row(): + with TreeViewsTest.test_session_context() as session: + # this needs to be run via django, but not directly via test_session_context + set_group_concat_max_len(connection.cursor()) + yield session + + with _run_for_row() as session: + results = get_tree_rows( + self.geographytreedef.id, "Geography", self.earth.id, "geographyid", False, session + ) + expected = [ + (self.na.id, self.na.name, self.na.fullname, self.na.nodenumber, self.na.highestchildnodenumber, self.na.rankid, None, None, 'NULL', self.na.children.count(), 'NA Syn 0, NA Syn 1'), + (na_syn_0.id, na_syn_0.name, na_syn_0.fullname, na_syn_0.nodenumber, na_syn_0.highestchildnodenumber, na_syn_0.rankid, self.na.id, self.na.fullname, 'NULL', na_syn_0.children.count(), None), + (na_syn_1.id, na_syn_1.name, na_syn_1.fullname, na_syn_1.nodenumber, na_syn_1.highestchildnodenumber, na_syn_1.rankid, self.na.id, self.na.fullname, 'NULL', na_syn_1.children.count(), None), + ] + + self.assertCountEqual( + results, + expected + ) + + with _run_for_row() as session: + results = get_tree_rows( + self.geographytreedef.id, "Geography", self.na.id, "name", False, session + ) + expected = [ + (self.usa.id, self.usa.name, self.usa.fullname, self.usa.nodenumber, self.usa.highestchildnodenumber, self.usa.rankid, None, None, 'NULL', self.usa.children.count(), 'USA Syn 0, USA Syn 1, USA Syn 2'), + (usa_syn_0.id, usa_syn_0.name, usa_syn_0.fullname, usa_syn_0.nodenumber, usa_syn_0.highestchildnodenumber, usa_syn_0.rankid, self.usa.id, self.usa.fullname, 'NULL', 0, None), + (usa_syn_1.id, usa_syn_1.name, usa_syn_1.fullname, usa_syn_1.nodenumber, usa_syn_1.highestchildnodenumber, usa_syn_1.rankid, self.usa.id, self.usa.fullname, 'NULL', 0, None), + (usa_syn_2.id, usa_syn_2. name, usa_syn_2.fullname, usa_syn_2.nodenumber, usa_syn_2.highestchildnodenumber, usa_syn_2.rankid, self.usa.id, self.usa.fullname, 'NULL', 0, None) + + ] + self.assertCountEqual( + results, + expected + ) class AddDeleteRankResourcesTest(ApiTests): def test_add_ranks_without_defaults(self): diff --git a/specifyweb/specify/tree_extras.py b/specifyweb/specify/tree_extras.py index b6d66bbb420..1658997eccc 100644 --- a/specifyweb/specify/tree_extras.py +++ b/specifyweb/specify/tree_extras.py @@ -91,7 +91,7 @@ def save(): "rankid" : self.parent.rankid, "fullName": self.parent.fullname, "parentid": self.parent.parent.id, - "children": list(self.parent.children.values('id', 'fullName')) + "children": list(self.parent.children.values('id', 'fullname')) } }) diff --git a/specifyweb/specify/tree_views.py b/specifyweb/specify/tree_views.py index 272017597e0..753a540f4f0 100644 --- a/specifyweb/specify/tree_views.py +++ b/specifyweb/specify/tree_views.py @@ -115,7 +115,7 @@ def wrapper(*args, **kwargs): }, { "type": "string", - "description": "concat of fullname of syns" + "description": "Concat of fullname of syonyms" } ], } @@ -133,6 +133,21 @@ def tree_view(request, treedef, tree, parentid, sortfield): the tree defined by treedefid = . The nodes are sorted according to . """ + """ + Also include the author of the node in the response if requested and the tree is the taxon tree. + There is a preference which can be enabled from within Specify which adds the author next to the + fullname on the front end. + See https://github.com/specify/specify7/pull/2818 for more context and a breakdown regarding + implementation/design decisions + """ + include_author = request.GET.get('includeauthor', False) and tree == 'taxon' + with models.session_context() as session: + set_group_concat_max_len(session.connection()) + results = get_tree_rows(treedef, tree, parentid, sortfield, include_author, session) + return HttpResponse(toJson(results), content_type='application/json') + + +def get_tree_rows(treedef, tree, parentid, sortfield, include_author, session): tree_table = datamodel.get_table(tree) parentid = None if parentid == 'null' else int(parentid) @@ -144,62 +159,48 @@ def tree_view(request, treedef, tree, parentid, sortfield): child_id = getattr(child, node._id) treedef_col = getattr(node, tree_table.name + "TreeDefID") orderby = tree_table.name.lower() + '.' + sortfield - - """ - Also include the author of the node in the response if requested and the tree is the taxon tree. - There is a preference which can be enabled from within Specify which adds the author next to the - fullname on the front end. - See https://github.com/specify/specify7/pull/2818 for more context and a breakdown regarding - implementation/design decisions - """ - includeAuthor = request.GET.get( - 'includeauthor') if 'includeauthor' in request.GET else False - - with models.session_context() as session: - set_group_concat_max_len(session) - - col_args = [ - node.name, - node.fullName, - node.nodeNumber, - node.highestChildNodeNumber, - node.rankId, - node.AcceptedID, - accepted.fullName, - node.author if (includeAuthor and tree == 'taxon') else "NULL", - ] - - apply_min = [ - # for some reason, SQL is rejecting the group_by in some dbs - # due to "only_full_group_by". It is somehow not smart enough to see - # that there is no dependency in the columns going from main table to the to-manys (child, and syns) - # I want to use ANY_VALUE() but that's not supported by MySQL 5.6- and MariaDB. - # I don't want to disable "only_full_group_by" in case someone misuses it... - # applying min to fool into thinking it is aggregated. - # these values are guarenteed to be the same - sql.func.min(arg) for arg in col_args - ] - grouped = [ - *apply_min, - # syns are to-many, so child can be duplicated - sql.func.count(distinct(child_id)), - # child are to-many, so syn's full name can be duplicated - # FEATURE: Allow users to select a separator?? Maybe that's too nice - group_concat(distinct(synonym.fullName), separator=', ') + col_args = [ + node.name, + node.fullName, + node.nodeNumber, + node.highestChildNodeNumber, + node.rankId, + node.AcceptedID, + accepted.fullName, + node.author if include_author else "NULL", + ] + + apply_min = [ + # for some reason, SQL is rejecting the group_by in some dbs + # due to "only_full_group_by". It is somehow not smart enough to see + # that there is no dependency in the columns going from main table to the to-manys (child, and syns) + # I want to use ANY_VALUE() but that's not supported by MySQL 5.6- and MariaDB. + # I don't want to disable "only_full_group_by" in case someone misuses it... + # applying min to fool into thinking it is aggregated. + # these values are guarenteed to be the same + sql.func.min(arg) for arg in col_args ] - - query = session.query(id_col, *grouped) \ - .outerjoin(child, child.ParentID == id_col) \ - .outerjoin(accepted, node.AcceptedID == getattr(accepted, node._id)) \ - .outerjoin(synonym, synonym.AcceptedID == id_col) \ - .group_by(id_col) \ - .filter(treedef_col == int(treedef)) \ - .filter(node.ParentID == parentid) \ - .order_by(orderby) - results = list(query) - return HttpResponse(toJson(results), content_type='application/json') - + + grouped = [ + *apply_min, + # syns are to-many, so child can be duplicated + sql.func.count(distinct(child_id)), + # child are to-many, so syn's full name can be duplicated + # FEATURE: Allow users to select a separator?? Maybe that's too nice + group_concat(distinct(synonym.fullName), separator=', ') + ] + + query = session.query(id_col, *grouped) \ + .outerjoin(child, child.ParentID == id_col) \ + .outerjoin(accepted, node.AcceptedID == getattr(accepted, node._id)) \ + .outerjoin(synonym, synonym.AcceptedID == id_col) \ + .group_by(id_col) \ + .filter(treedef_col == int(treedef)) \ + .filter(node.ParentID == parentid) \ + .order_by(orderby) + results = list(query) + return results @login_maybe_required @require_GET diff --git a/specifyweb/stored_queries/execution.py b/specifyweb/stored_queries/execution.py index 2aaa201dcab..71008e5a6a3 100644 --- a/specifyweb/stored_queries/execution.py +++ b/specifyweb/stored_queries/execution.py @@ -30,12 +30,12 @@ SORT_TYPES = [None, asc, desc] -def set_group_concat_max_len(session): +def set_group_concat_max_len(connection): """The default limit on MySQL group concat function is quite small. This function increases it for the database connection for the given session. """ - session.connection().execute('SET group_concat_max_len = 1024 * 1024 * 1024') + connection.execute('SET group_concat_max_len = 1024 * 1024 * 1024') def filter_by_collection(model, query, collection): """Add predicates to the given query to filter result to items scoped @@ -190,7 +190,7 @@ def query_to_csv(session, collection, user, tableid, field_specs, path, See build_query for details of the other accepted arguments. """ - set_group_concat_max_len(session) + set_group_concat_max_len(session.connection()) query, __ = build_query(session, collection, user, tableid, field_specs, recordsetid, replace_nulls=True, distinct=distinct) logger.debug('query_to_csv starting') @@ -227,7 +227,7 @@ def query_to_kml(session, collection, user, tableid, field_specs, path, captions See build_query for details of the other accepted arguments. """ - set_group_concat_max_len(session) + set_group_concat_max_len(session.connection()) query, __ = build_query(session, collection, user, tableid, field_specs, recordsetid, replace_nulls=True) logger.debug('query_to_kml starting') @@ -529,7 +529,7 @@ def return_loan_preps(collection, user, agent, data): def execute(session, collection, user, tableid, distinct, count_only, field_specs, limit, offset, recordsetid=None, formatauditobjs=False): "Build and execute a query, returning the results as a data structure for json serialization" - set_group_concat_max_len(session) + set_group_concat_max_len(session.connection()) query, order_by_exprs = build_query(session, collection, user, tableid, field_specs, recordsetid=recordsetid, formatauditobjs=formatauditobjs, distinct=distinct) if count_only: diff --git a/specifyweb/stored_queries/tests.py b/specifyweb/stored_queries/tests.py index f494b548652..43ad13b0b0c 100644 --- a/specifyweb/stored_queries/tests.py +++ b/specifyweb/stored_queries/tests.py @@ -55,7 +55,6 @@ def test_stringid_roundtrip_en_masse(self) -> None: """ - class SQLAlchemySetup(ApiTests): test_sa_url = None @@ -85,7 +84,7 @@ def run_django_query(conn, cursor, statement, parameters, context, executemany): columns = django_cursor.description django_cursor.close() # SqlAlchemy needs to find columns back in the rows, hence adding label to columns - selects = [sqlalchemy.select([sqlalchemy.literal(column).label(columns[idx][0]) for idx, column in enumerate(row)]) for row + selects = [sqlalchemy.select([sqlalchemy.literal(sqlalchemy.null() if column is None else column).label(columns[idx][0]) for idx, column in enumerate(row)]) for row in result_set] # union all instead of union because rows can be duplicated in the original query, # but still need to preserve the duplication @@ -95,7 +94,6 @@ def run_django_query(conn, cursor, statement, parameters, context, executemany): return final_query, () - class SQLAlchemySetupTest(SQLAlchemySetup): def test_collection_object_count(self): From cd0bec92ba82e101535115d59f2e60e5dbca75db Mon Sep 17 00:00:00 2001 From: realVinayak Date: Wed, 17 Jul 2024 12:11:31 -0500 Subject: [PATCH 08/27] Remove defunct comment --- specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx index 0b6c38bf58c..06ce2e0220f 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx @@ -230,8 +230,7 @@ export function TreeRow({ ? treeText.acceptedName({ name: row.acceptedName ?? row.acceptedId.toString(), }) - : // Backend will never return undefined... - typeof row.synonyms === 'string' + :typeof row.synonyms === 'string' ? treeText.synonyms({ names: row.synonyms, }) From fe9e310dcb88d9e8155ca3aa7e810315a2a63acf Mon Sep 17 00:00:00 2001 From: realVinayak Date: Wed, 17 Jul 2024 17:12:35 -0500 Subject: [PATCH 09/27] Minor type resolve --- specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts b/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts index 544cce3fdfb..cc80f8a4ed3 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts +++ b/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts @@ -24,9 +24,9 @@ export const fetchRows = async (fetchUrl: string) => number, number | null, string | null, - string, + string | null, number, - string + string | null ] > >(fetchUrl, { From 6cacdcf6d3a691a66292ae2d11ce0b149299ada5 Mon Sep 17 00:00:00 2001 From: realVinayak Date: Wed, 17 Jul 2024 22:16:14 +0000 Subject: [PATCH 10/27] Lint code with ESLint and Prettier Triggered by 57cf79106580885aebf8ad1bbd60ce6a587d2524 on branch refs/heads/issue-5067 --- specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts b/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts index cc80f8a4ed3..069fd4dc0cc 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts +++ b/specifyweb/frontend/js_src/lib/components/TreeView/helpers.ts @@ -45,7 +45,7 @@ export const fetchRows = async (fetchUrl: string) => acceptedName = undefined, author = undefined, children, - synonyms = undefined + synonyms = undefined, ], index, { length } @@ -61,7 +61,7 @@ export const fetchRows = async (fetchUrl: string) => author, children, isLastChild: index + 1 === length, - synonyms + synonyms, }) ) ); From 54b61eefb108b4bd93f25f29c5eba6dd97c26fca Mon Sep 17 00:00:00 2001 From: realVinayak Date: Thu, 18 Jul 2024 17:12:44 +0000 Subject: [PATCH 11/27] Lint code with ESLint and Prettier Triggered by 6cacdcf6d3a691a66292ae2d11ce0b149299ada5 on branch refs/heads/issue-5067-b --- specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx index 06ce2e0220f..902deb8d8fa 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx @@ -230,7 +230,7 @@ export function TreeRow({ ? treeText.acceptedName({ name: row.acceptedName ?? row.acceptedId.toString(), }) - :typeof row.synonyms === 'string' + : typeof row.synonyms === 'string' ? treeText.synonyms({ names: row.synonyms, }) From af05c4fca22ffd99ca7bc1643aa4ced31d2791f0 Mon Sep 17 00:00:00 2001 From: realVinayak Date: Mon, 22 Jul 2024 08:25:05 -0500 Subject: [PATCH 12/27] Redo timestamp modified/created setting --- specifyweb/attachment_gw/models.py | 5 - specifyweb/specify/build_models.py | 35 +-- specifyweb/specify/model_extras.py | 6 +- specifyweb/specify/model_timestamp.py | 53 +--- specifyweb/specify/models.py | 360 +++++++++++++------------- specifyweb/specify/tree_extras.py | 2 +- specifyweb/workbench/models.py | 2 - 7 files changed, 199 insertions(+), 264 deletions(-) diff --git a/specifyweb/attachment_gw/models.py b/specifyweb/attachment_gw/models.py index f59cfd98d81..6d5573b4f4d 100644 --- a/specifyweb/attachment_gw/models.py +++ b/specifyweb/attachment_gw/models.py @@ -1,8 +1,4 @@ from django.db import models -from django.conf import settings -from django.db.models.deletion import CASCADE, SET_NULL -from django.utils import timezone -from model_utils import FieldTracker from functools import partialmethod from specifyweb.specify.models import datamodel, custom_save from ..workbench.models import Dataset @@ -15,7 +11,6 @@ class Spattachmentdataset(Dataset): class Meta: db_table = 'attachmentdataset' - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) save = partialmethod(custom_save) # from django.apps import apps diff --git a/specifyweb/specify/build_models.py b/specifyweb/specify/build_models.py index 2200a9dc9b0..60c5aff7f78 100644 --- a/specifyweb/specify/build_models.py +++ b/specifyweb/specify/build_models.py @@ -1,12 +1,9 @@ from django.db import models from django.db.models.signals import pre_delete -from model_utils import FieldTracker -from requests import get - from specifyweb.businessrules.exceptions import AbortSave from . import model_extras -from .model_timestamp import pre_save_auto_timestamp_field_with_override +from .model_timestamp import pre_save_auto_timestamp_field_with_override, timestamp_fields appname = __name__.split('.')[-2] @@ -62,8 +59,12 @@ class Meta: if 'rankid' in attrs: ordering += ('rankid', ) + has_timestamp_fields = any(field.lower() in timestamp_fields for field in table.fields) + def save(self, *args, **kwargs): try: + if has_timestamp_fields: + pre_save_auto_timestamp_field_with_override(self) return super(model, self).save(*args, **kwargs) except AbortSave: return @@ -76,40 +77,16 @@ def pre_constraints_delete(self): # This is not currently used, but is here for future use. pre_delete.send(sender=self.__class__, instance=self) - def save_timestamped(self, *args, **kwargs): - timestamp_override = kwargs.pop('timestamp_override', False) - pre_save_auto_timestamp_field_with_override(self, timestamp_override) - try: - super(model, self).save(*args, **kwargs) - except AbortSave: - return - - field_names = [field.name.lower() for field in table.fields] - timestamp_fields = ['timestampcreated', 'timestampmodified'] - has_timestamp_fields = any(field in field_names for field in timestamp_fields) - - if has_timestamp_fields: - tracked_fields = [field for field in timestamp_fields if field in field_names] - attrs['timestamptracker'] = FieldTracker(fields=tracked_fields) - for field in tracked_fields: - attrs[field] = models.DateTimeField(db_column=field) # default=timezone.now is handled in pre_save_auto_timestamp_field_with_override - attrs['Meta'] = Meta if table.django_name in tables_with_pre_constraints_delete: # This is not currently used, but is here for future use. attrs['pre_constraints_delete'] = pre_constraints_delete - if has_timestamp_fields: - attrs['save'] = save_timestamped - else: - attrs['save'] = save + attrs['save'] = save supercls = models.Model if hasattr(model_extras, table.django_name): supercls = getattr(model_extras, table.django_name) - elif has_timestamp_fields: - # FUTURE: supercls = SpTimestampedModel - pass model = type(table.django_name, (supercls,), attrs) return model diff --git a/specifyweb/specify/model_extras.py b/specifyweb/specify/model_extras.py index 0f3bea3cf96..017e7f1462d 100644 --- a/specifyweb/specify/model_extras.py +++ b/specifyweb/specify/model_extras.py @@ -5,7 +5,7 @@ from django.conf import settings from django.utils import timezone -from .model_timestamp import SpTimestampedModel, pre_save_auto_timestamp_field_with_override +from .model_timestamp import pre_save_auto_timestamp_field_with_override from .tree_extras import Tree, TreeRank if settings.AUTH_LDAP_SERVER_URI is not None: @@ -20,7 +20,7 @@ def create_user(self, name, password=None): def create_superuser(self, name, password=None): raise NotImplementedError() -class Specifyuser(models.Model): # FUTURE: class Specifyuser(SpTimestampedModel): +class Specifyuser(models.Model): USERNAME_FIELD = 'name' REQUIRED_FIELDS = [] is_active = True @@ -125,7 +125,7 @@ class Meta: -class Preparation(models.Model): # FUTURE: class Preparation(SpTimestampedModel): +class Preparation(models.Model): def isonloan(self): # TODO: needs unit tests from django.db import connection diff --git a/specifyweb/specify/model_timestamp.py b/specifyweb/specify/model_timestamp.py index 1acf5d0dbd7..55d064dfd24 100644 --- a/specifyweb/specify/model_timestamp.py +++ b/specifyweb/specify/model_timestamp.py @@ -2,50 +2,17 @@ from django.utils import timezone from django.conf import settings -from model_utils import FieldTracker -def pre_save_auto_timestamp_field_with_override(obj, timestamp_override=None): - # Normal behavior is to update the timestamps automatically when saving. - # If timestampcreated or timestampmodified have been edited, don't update them to the current time. - cur_time = timezone.now() - timestamp_override = ( - timestamp_override - if timestamp_override is not None - else getattr(settings, "TIMESTAMP_SAVE_OVERRIDE", False) - ) - timestamp_fields = ['timestampcreated', 'timestampmodified'] - for field in timestamp_fields: - if hasattr(obj, field) and hasattr(obj, 'timestamptracker'): - if not timestamp_override and field not in obj.timestamptracker.changed() and \ - (not obj.id or not getattr(obj, field)): - setattr(obj, field, cur_time) - elif timestamp_override and not getattr(obj, field): - setattr(obj, field, cur_time) - - avoid_null_timestamp_fields(obj) - -def avoid_null_timestamp_fields(obj): - cur_time = timezone.now() - if hasattr(obj, 'timestampcreated') and getattr(obj, 'timestampcreated') is None: - obj.timestampcreated = cur_time - if hasattr(obj, 'timestampmodified') and getattr(obj, 'timestampmodified') is None: - obj.timestampmodified = cur_time -# NOTE: This class is needed for when we get rid of dynamic model creation from Specify 6 datamodel.xml file. -# NOTE: Currently in sperate file to avoid circular import. -class SpTimestampedModel(models.Model): - """ - SpTimestampedModel(id, timestampcreated, timestampmodified) - """ +timestamp_fields = {'timestampmodified', 'timestampcreated'} - timestampcreated = models.DateTimeField(db_column='TimestampCreated', default=timezone.now) - timestampmodified = models.DateTimeField(db_column='TimestampModified', default=timezone.now) +def pre_save_auto_timestamp_field_with_override(obj): + # If object already is present, reset timestamps to null. - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) - - class Meta: - abstract = True - - def save(self, *args, **kwargs): - pre_save_auto_timestamp_field_with_override(self) - super().save(*args, **kwargs) + if obj.id is None: + return + + for field in timestamp_fields: + if not hasattr(obj, field): + continue + setattr(obj, field, None) \ No newline at end of file diff --git a/specifyweb/specify/models.py b/specifyweb/specify/models.py index 78355910e32..85a84322848 100644 --- a/specifyweb/specify/models.py +++ b/specifyweb/specify/models.py @@ -1,7 +1,6 @@ from functools import partialmethod from django.db import models from django.utils import timezone -from model_utils import FieldTracker from specifyweb.businessrules.exceptions import AbortSave from specifyweb.specify.model_timestamp import pre_save_auto_timestamp_field_with_override from specifyweb.specify import model_extras @@ -77,7 +76,7 @@ class Meta: # models.Index(fields=['DateAccessioned'], name='AccessionDateIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Accessionagent(models.Model): @@ -104,7 +103,7 @@ class Meta: db_table = 'accessionagent' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Accessionattachment(models.Model): @@ -130,7 +129,7 @@ class Meta: db_table = 'accessionattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Accessionauthorization(models.Model): @@ -156,7 +155,7 @@ class Meta: db_table = 'accessionauthorization' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Accessioncitation(models.Model): @@ -185,7 +184,7 @@ class Meta: db_table = 'accessioncitation' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Address(models.Model): @@ -230,7 +229,7 @@ class Meta: db_table = 'address' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Addressofrecord(models.Model): @@ -260,7 +259,7 @@ class Meta: db_table = 'addressofrecord' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Agent(models.Model): @@ -328,7 +327,7 @@ class Meta: # models.Index(fields=['Abbreviation'], name='AbbreviationIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Agentattachment(models.Model): @@ -354,7 +353,7 @@ class Meta: db_table = 'agentattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Agentgeography(models.Model): @@ -380,7 +379,7 @@ class Meta: db_table = 'agentgeography' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Agentidentifier(models.Model): @@ -420,7 +419,7 @@ class Meta: db_table = 'agentidentifier' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Agentspecialty(models.Model): @@ -445,7 +444,7 @@ class Meta: db_table = 'agentspecialty' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Agentvariant(models.Model): @@ -473,7 +472,7 @@ class Meta: db_table = 'agentvariant' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Appraisal(models.Model): @@ -506,7 +505,7 @@ class Meta: # models.Index(fields=['AppraisalDate'], name='AppraisalDateIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Attachment(models.Model): @@ -562,7 +561,7 @@ class Meta: # models.Index(fields=['GUID'], name='AttchmentGuidIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Attachmentimageattribute(models.Model): @@ -602,7 +601,7 @@ class Meta: db_table = 'attachmentimageattribute' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Attachmentmetadata(models.Model): @@ -627,7 +626,7 @@ class Meta: db_table = 'attachmentmetadata' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Attachmenttag(models.Model): @@ -651,7 +650,7 @@ class Meta: db_table = 'attachmenttag' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Attributedef(models.Model): @@ -678,7 +677,7 @@ class Meta: db_table = 'attributedef' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Author(models.Model): @@ -704,7 +703,7 @@ class Meta: db_table = 'author' ordering = ('ordernumber',) - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Autonumberingscheme(models.Model): @@ -734,7 +733,7 @@ class Meta: # models.Index(fields=['SchemeName'], name='SchemeNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Borrow(models.Model): @@ -781,7 +780,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='BorColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Borrowagent(models.Model): @@ -811,7 +810,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='BorColMemIDX2') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Borrowattachment(models.Model): @@ -837,7 +836,7 @@ class Meta: db_table = 'borrowattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Borrowmaterial(models.Model): @@ -875,7 +874,7 @@ class Meta: # models.Index(fields=['Description'], name='DescriptionIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Borrowreturnmaterial(models.Model): @@ -907,7 +906,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='BorrowReturnedColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectingevent(models.Model): @@ -976,7 +975,7 @@ class Meta: # models.Index(fields=['GUID'], name='CEGuidIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectingeventattachment(models.Model): @@ -1006,7 +1005,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='CEAColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectingeventattr(models.Model): @@ -1036,7 +1035,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='COLEVATColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectingeventattribute(models.Model): @@ -1109,7 +1108,7 @@ class Meta: # models.Index(fields=['DisciplineID'], name='COLEVATSDispIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectingeventauthorization(models.Model): @@ -1134,7 +1133,7 @@ class Meta: db_table = 'collectingeventauthorization' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectingtrip(models.Model): @@ -1195,7 +1194,7 @@ class Meta: # models.Index(fields=['StartDate'], name='COLTRPStartDateIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectingtripattachment(models.Model): @@ -1225,7 +1224,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='CTAColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectingtripattribute(models.Model): @@ -1297,7 +1296,7 @@ class Meta: # models.Index(fields=['DisciplineID'], name='COLTRPSDispIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectingtripauthorization(models.Model): @@ -1322,7 +1321,7 @@ class Meta: db_table = 'collectingtripauthorization' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collection(models.Model): @@ -1372,7 +1371,7 @@ class Meta: # models.Index(fields=['GUID'], name='CollectionGuidIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectionobject(models.Model): @@ -1474,7 +1473,7 @@ class Meta: # models.Index(fields=['CollectionmemberID'], name='COColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectionobjectattachment(models.Model): @@ -1504,7 +1503,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='COLOBJATTColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectionobjectattr(models.Model): @@ -1534,7 +1533,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='COLOBJATRSColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectionobjectattribute(models.Model): @@ -1681,7 +1680,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='COLOBJATTRSColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectionobjectcitation(models.Model): @@ -1714,7 +1713,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='COCITColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectionobjectproperty(models.Model): @@ -1903,7 +1902,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='COLOBJPROPColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectionreltype(models.Model): @@ -1929,7 +1928,7 @@ class Meta: db_table = 'collectionreltype' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collectionrelationship(models.Model): @@ -1956,7 +1955,7 @@ class Meta: db_table = 'collectionrelationship' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Collector(models.Model): @@ -1991,7 +1990,7 @@ class Meta: # models.Index(fields=['DivisionID'], name='COLTRDivIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Commonnametx(models.Model): @@ -2023,7 +2022,7 @@ class Meta: # models.Index(fields=['Country'], name='CommonNameTxCountryIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Commonnametxcitation(models.Model): @@ -2058,7 +2057,7 @@ class Meta: db_table = 'commonnametxcitation' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Conservdescription(models.Model): @@ -2130,7 +2129,7 @@ class Meta: # models.Index(fields=['ShortDesc'], name='ConservDescShortDescIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Conservdescriptionattachment(models.Model): @@ -2156,7 +2155,7 @@ class Meta: db_table = 'conservdescriptionattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Conservevent(models.Model): @@ -2207,7 +2206,7 @@ class Meta: # models.Index(fields=['completedDate'], name='ConservCompletedDateIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Conserveventattachment(models.Model): @@ -2233,7 +2232,7 @@ class Meta: db_table = 'conserveventattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Container(models.Model): @@ -2266,7 +2265,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='ContainerMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Dnaprimer(models.Model): @@ -2316,7 +2315,7 @@ class Meta: # models.Index(fields=['PrimerDesignator'], name='DesignatorIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Dnasequence(models.Model): @@ -2376,7 +2375,7 @@ class Meta: # models.Index(fields=['BOLDSampleID'], name='BOLDSampleIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Dnasequenceattachment(models.Model): @@ -2402,7 +2401,7 @@ class Meta: db_table = 'dnasequenceattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Dnasequencingrun(models.Model): @@ -2458,7 +2457,7 @@ class Meta: db_table = 'dnasequencingrun' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Dnasequencingrunattachment(models.Model): @@ -2484,7 +2483,7 @@ class Meta: db_table = 'dnasequencerunattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Dnasequencingruncitation(models.Model): @@ -2519,7 +2518,7 @@ class Meta: db_table = 'dnasequencingruncitation' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Datatype(models.Model): @@ -2542,7 +2541,7 @@ class Meta: db_table = 'datatype' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Deaccession(models.Model): @@ -2597,7 +2596,7 @@ class Meta: # models.Index(fields=['DeaccessionDate'], name='DeaccessionDateIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Deaccessionagent(models.Model): @@ -2623,7 +2622,7 @@ class Meta: db_table = 'deaccessionagent' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Deaccessionattachment(models.Model): @@ -2649,7 +2648,7 @@ class Meta: db_table = 'deaccessionattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Determination(models.Model): @@ -2721,7 +2720,7 @@ class Meta: # models.Index(fields=['TypeStatusName'], name='TypeStatusNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Determinationcitation(models.Model): @@ -2754,7 +2753,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='DetCitColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Determiner(models.Model): @@ -2785,7 +2784,7 @@ class Meta: db_table = 'determiner' ordering = ('ordernumber',) - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Discipline(models.Model): @@ -2823,7 +2822,7 @@ class Meta: # models.Index(fields=['Name'], name='DisciplineNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Disposal(models.Model): @@ -2861,7 +2860,7 @@ class Meta: # models.Index(fields=['DisposalDate'], name='DisposalDateIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Disposalagent(models.Model): @@ -2887,7 +2886,7 @@ class Meta: db_table = 'disposalagent' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Disposalattachment(models.Model): @@ -2913,7 +2912,7 @@ class Meta: db_table = 'disposalattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Disposalpreparation(models.Model): @@ -2940,7 +2939,7 @@ class Meta: db_table = 'disposalpreparation' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Division(models.Model): @@ -2976,7 +2975,7 @@ class Meta: # models.Index(fields=['Name'], name='DivisionNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Exchangein(models.Model): @@ -3020,7 +3019,7 @@ class Meta: # models.Index(fields=['DescriptionOfMaterial'], name='DescriptionOfMaterialIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Exchangeinattachment(models.Model): @@ -3046,7 +3045,7 @@ class Meta: db_table = 'exchangeinattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Exchangeinprep(models.Model): @@ -3080,7 +3079,7 @@ class Meta: # models.Index(fields=['DisciplineID'], name='ExchgInPrepDspMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Exchangeout(models.Model): @@ -3126,7 +3125,7 @@ class Meta: # models.Index(fields=['ExchangeOutNumber'], name='ExchangeOutNumberIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Exchangeoutattachment(models.Model): @@ -3152,7 +3151,7 @@ class Meta: db_table = 'exchangeoutattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Exchangeoutprep(models.Model): @@ -3186,7 +3185,7 @@ class Meta: # models.Index(fields=['DisciplineID'], name='ExchgOutPrepDspMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Exsiccata(models.Model): @@ -3212,7 +3211,7 @@ class Meta: db_table = 'exsiccata' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Exsiccataitem(models.Model): @@ -3238,7 +3237,7 @@ class Meta: db_table = 'exsiccataitem' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Extractor(models.Model): @@ -3268,7 +3267,7 @@ class Meta: db_table = 'extractor' ordering = ('ordernumber',) - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Fieldnotebook(models.Model): @@ -3303,7 +3302,7 @@ class Meta: # models.Index(fields=['EndDate'], name='FNBEndDateIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Fieldnotebookattachment(models.Model): @@ -3329,7 +3328,7 @@ class Meta: db_table = 'fieldnotebookattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Fieldnotebookpage(models.Model): @@ -3360,7 +3359,7 @@ class Meta: # models.Index(fields=['ScanDate'], name='FNBPScanDateIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Fieldnotebookpageattachment(models.Model): @@ -3386,7 +3385,7 @@ class Meta: db_table = 'fieldnotebookpageattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Fieldnotebookpageset(models.Model): @@ -3420,7 +3419,7 @@ class Meta: # models.Index(fields=['EndDate'], name='FNBPSEndDateIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Fieldnotebookpagesetattachment(models.Model): @@ -3446,7 +3445,7 @@ class Meta: db_table = 'fieldnotebookpagesetattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Fundingagent(models.Model): @@ -3478,7 +3477,7 @@ class Meta: # models.Index(fields=['DivisionID'], name='COLTRIPDivIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Geocoorddetail(models.Model): @@ -3540,7 +3539,7 @@ class Meta: db_table = 'geocoorddetail' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Geography(model_extras.Geography): @@ -3590,7 +3589,7 @@ class Meta: # models.Index(fields=['FullName'], name='GeoFullNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Geographytreedef(models.Model): @@ -3615,7 +3614,7 @@ class Meta: db_table = 'geographytreedef' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Geographytreedefitem(model_extras.Geographytreedefitem): @@ -3648,7 +3647,7 @@ class Meta: db_table = 'geographytreedefitem' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Geologictimeperiod(model_extras.Geologictimeperiod): @@ -3695,7 +3694,7 @@ class Meta: # models.Index(fields=['GUID'], name='GTPGuidIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Geologictimeperiodtreedef(models.Model): @@ -3720,7 +3719,7 @@ class Meta: db_table = 'geologictimeperiodtreedef' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Geologictimeperiodtreedefitem(model_extras.Geologictimeperiodtreedefitem): @@ -3753,7 +3752,7 @@ class Meta: db_table = 'geologictimeperiodtreedefitem' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Gift(models.Model): @@ -3809,7 +3808,7 @@ class Meta: # models.Index(fields=['GiftDate'], name='GiftDateIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Giftagent(models.Model): @@ -3840,7 +3839,7 @@ class Meta: # models.Index(fields=['DisciplineID'], name='GiftAgDspMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Giftattachment(models.Model): @@ -3866,7 +3865,7 @@ class Meta: db_table = 'giftattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Giftpreparation(models.Model): @@ -3904,7 +3903,7 @@ class Meta: # models.Index(fields=['DisciplineID'], name='GiftPrepDspMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Groupperson(models.Model): @@ -3931,7 +3930,7 @@ class Meta: db_table = 'groupperson' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Inforequest(models.Model): @@ -3966,7 +3965,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='IRColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Institution(models.Model): @@ -4020,7 +4019,7 @@ class Meta: # models.Index(fields=['GUID'], name='InstGuidIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Institutionnetwork(models.Model): @@ -4058,7 +4057,7 @@ class Meta: # models.Index(fields=['Name'], name='InstNetworkNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Journal(models.Model): @@ -4091,7 +4090,7 @@ class Meta: # models.Index(fields=['GUID'], name='JournalGUIDIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Latlonpolygon(models.Model): @@ -4118,7 +4117,7 @@ class Meta: db_table = 'latlonpolygon' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Latlonpolygonpnt(models.Model): @@ -4184,7 +4183,7 @@ class Meta: # models.Index(fields=['GUID'], name='LithoGuidIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Lithostrattreedef(models.Model): @@ -4209,7 +4208,7 @@ class Meta: db_table = 'lithostrattreedef' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Lithostrattreedefitem(model_extras.Lithostrattreedefitem): @@ -4242,7 +4241,7 @@ class Meta: db_table = 'lithostrattreedefitem' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Loan(models.Model): @@ -4301,7 +4300,7 @@ class Meta: # models.Index(fields=['CurrentDueDate'], name='CurrentDueDateIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Loanagent(models.Model): @@ -4331,7 +4330,7 @@ class Meta: # models.Index(fields=['DisciplineID'], name='LoanAgDspMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Loanattachment(models.Model): @@ -4357,7 +4356,7 @@ class Meta: db_table = 'loanattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Loanpreparation(models.Model): @@ -4398,7 +4397,7 @@ class Meta: # models.Index(fields=['DisciplineID'], name='LoanPrepDspMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Loanreturnpreparation(models.Model): @@ -4431,7 +4430,7 @@ class Meta: # models.Index(fields=['DisciplineID'], name='LoanRetPrepDspMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Locality(models.Model): @@ -4506,7 +4505,7 @@ class Meta: # models.Index(fields=['RelationToNamedPlace'], name='RelationToNamedPlaceIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Localityattachment(models.Model): @@ -4532,7 +4531,7 @@ class Meta: db_table = 'localityattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Localitycitation(models.Model): @@ -4565,7 +4564,7 @@ class Meta: # models.Index(fields=['DisciplineID'], name='LocCitDspMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Localitydetail(models.Model): @@ -4635,7 +4634,7 @@ class Meta: db_table = 'localitydetail' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Localitynamealias(models.Model): @@ -4664,7 +4663,7 @@ class Meta: # models.Index(fields=['Name'], name='LocalityNameAliasIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Materialsample(models.Model): @@ -4731,7 +4730,7 @@ class Meta: # models.Index(fields=['GGBNSampleDesignation'], name='DesignationIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Morphbankview(models.Model): @@ -4762,7 +4761,7 @@ class Meta: db_table = 'morphbankview' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Otheridentifier(models.Model): @@ -4808,7 +4807,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='OthIdColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Paleocontext(models.Model): @@ -4856,7 +4855,7 @@ class Meta: # models.Index(fields=['DisciplineID'], name='PaleoCxtDisciplineIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Pcrperson(models.Model): @@ -4886,7 +4885,7 @@ class Meta: db_table = 'pcrperson' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Permit(models.Model): @@ -4938,7 +4937,7 @@ class Meta: # models.Index(fields=['IssuedDate'], name='IssuedDateIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Permitattachment(models.Model): @@ -4964,7 +4963,7 @@ class Meta: db_table = 'permitattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Picklist(models.Model): @@ -5001,7 +5000,7 @@ class Meta: # models.Index(fields=['Name'], name='PickListNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Picklistitem(models.Model): @@ -5027,7 +5026,7 @@ class Meta: db_table = 'picklistitem' ordering = ('ordinal',) - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Preptype(models.Model): @@ -5052,7 +5051,7 @@ class Meta: db_table = 'preptype' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Preparation(model_extras.Preparation): @@ -5128,7 +5127,7 @@ class Meta: # models.Index(fields=['BarCode'], name='PrepBarCodeIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Preparationattachment(models.Model): @@ -5158,7 +5157,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='PrepAttColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Preparationattr(models.Model): @@ -5188,7 +5187,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='PrepAttrColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Preparationattribute(models.Model): @@ -5255,7 +5254,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='PrepAttrsColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Preparationproperty(models.Model): @@ -5444,7 +5443,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='PREPPROPColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Project(models.Model): @@ -5487,7 +5486,7 @@ class Meta: # models.Index(fields=['ProjectNumber'], name='ProjectNumberIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Recordset(models.Model): @@ -5523,7 +5522,7 @@ class Meta: # models.Index(fields=['name'], name='RecordSetNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Recordsetitem(models.Model): @@ -5594,7 +5593,7 @@ class Meta: # models.Index(fields=['ISBN'], name='ISBNIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Referenceworkattachment(models.Model): @@ -5620,7 +5619,7 @@ class Meta: db_table = 'referenceworkattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Repositoryagreement(models.Model): @@ -5662,7 +5661,7 @@ class Meta: # models.Index(fields=['StartDate'], name='RefWrkStartDate') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Repositoryagreementattachment(models.Model): @@ -5688,7 +5687,7 @@ class Meta: db_table = 'repositoryagreementattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Shipment(models.Model): @@ -5737,7 +5736,7 @@ class Meta: # models.Index(fields=['ShipmentMethod'], name='ShipmentMethodIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spappresource(models.Model): @@ -5772,8 +5771,7 @@ class Meta: # models.Index(fields=['Name'], name='SpAppResNameIDX'), # models.Index(fields=['MimeType'], name='SpAppResMimeTypeIDX') ] - - timestamptracker = FieldTracker(fields=['timestampcreated']) + save = partialmethod(custom_save) class Spappresourcedata(models.Model): @@ -5827,7 +5825,7 @@ def save_spappresourcedata(self, *args, **kwargs): # else: # self._data = value - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(save_spappresourcedata) class Spappresourcedir(models.Model): @@ -5858,7 +5856,7 @@ class Meta: # models.Index(fields=['DisciplineType'], name='SpAppResourceDirDispTypeIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spauditlog(models.Model): @@ -5891,7 +5889,7 @@ def save_spauditlog(self, *args, **kwargs): self.recordversion = 0 # or some other default value custom_save(self, *args, **kwargs) - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(save_spauditlog) class Spauditlogfield(models.Model): @@ -5917,7 +5915,7 @@ class Meta: db_table = 'spauditlogfield' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spexportschema(models.Model): @@ -5943,7 +5941,7 @@ class Meta: db_table = 'spexportschema' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spexportschemaitem(models.Model): @@ -5971,7 +5969,7 @@ class Meta: db_table = 'spexportschemaitem' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spexportschemaitemmapping(models.Model): @@ -6000,7 +5998,7 @@ class Meta: db_table = 'spexportschemaitemmapping' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spexportschemamapping(models.Model): @@ -6029,7 +6027,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='SPEXPSCHMMAPColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spfieldvaluedefault(models.Model): @@ -6059,7 +6057,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='SpFieldValueDefaultColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Splocalecontainer(models.Model): @@ -6095,7 +6093,7 @@ class Meta: # models.Index(fields=['Name'], name='SpLocaleContainerNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Splocalecontaineritem(models.Model): @@ -6130,7 +6128,7 @@ class Meta: # models.Index(fields=['Name'], name='SpLocaleContainerItemNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Splocaleitemstr(models.Model): @@ -6164,7 +6162,7 @@ class Meta: # models.Index(fields=['Country'], name='SpLocaleCountyIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Sppermission(models.Model): @@ -6210,7 +6208,7 @@ class Meta: db_table = 'spprincipal' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spquery(models.Model): @@ -6248,7 +6246,7 @@ class Meta: # models.Index(fields=['Name'], name='SpQueryNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spqueryfield(models.Model): @@ -6289,7 +6287,7 @@ class Meta: db_table = 'spqueryfield' ordering = ('position',) - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spreport(models.Model): @@ -6324,7 +6322,7 @@ class Meta: # models.Index(fields=['Name'], name='SpReportNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spsymbiotainstance(models.Model): @@ -6358,7 +6356,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='SPSYMINSTColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Sptasksemaphore(models.Model): @@ -6390,7 +6388,7 @@ class Meta: db_table = 'sptasksemaphore' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spversion(models.Model): @@ -6418,7 +6416,7 @@ class Meta: db_table = 'spversion' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spviewsetobj(models.Model): @@ -6449,7 +6447,7 @@ class Meta: # models.Index(fields=['Name'], name='SpViewObjNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Spvisualquery(models.Model): @@ -6477,7 +6475,7 @@ class Meta: # models.Index(fields=['Name'], name='SpVisualQueryNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Specifyuser(model_extras.Specifyuser): @@ -6509,7 +6507,7 @@ class Meta: db_table = 'specifyuser' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + # save = partialmethod(custom_save) class Storage(model_extras.Storage): @@ -6552,7 +6550,7 @@ class Meta: # models.Index(fields=['FullName'], name='StorFullNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Storageattachment(models.Model): @@ -6578,7 +6576,7 @@ class Meta: db_table = 'storageattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Storagetreedef(models.Model): @@ -6603,7 +6601,7 @@ class Meta: db_table = 'storagetreedef' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Storagetreedefitem(model_extras.Storagetreedefitem): @@ -6636,7 +6634,7 @@ class Meta: db_table = 'storagetreedefitem' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Taxon(model_extras.Taxon): @@ -6756,7 +6754,7 @@ class Meta: # models.Index(fields=['EnvironmentalProtectionStatus'], name='EnvironmentalProtectionStatusIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Taxonattachment(models.Model): @@ -6782,7 +6780,7 @@ class Meta: db_table = 'taxonattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Taxonattribute(models.Model): @@ -6968,7 +6966,7 @@ class Meta: db_table = 'taxonattribute' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Taxoncitation(models.Model): @@ -7003,7 +7001,7 @@ class Meta: db_table = 'taxoncitation' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Taxontreedef(models.Model): @@ -7030,7 +7028,7 @@ class Meta: db_table = 'taxontreedef' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Taxontreedefitem(model_extras.Taxontreedefitem): @@ -7064,7 +7062,7 @@ class Meta: db_table = 'taxontreedefitem' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Treatmentevent(models.Model): @@ -7122,7 +7120,7 @@ class Meta: # models.Index(fields=['TreatmentNumber'], name='TETreatmentNumberIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Treatmenteventattachment(models.Model): @@ -7148,7 +7146,7 @@ class Meta: db_table = 'treatmenteventattachment' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Voucherrelationship(models.Model): @@ -7192,7 +7190,7 @@ class Meta: # models.Index(fields=['CollectionMemberID'], name='VRXDATColMemIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Workbench(models.Model): @@ -7231,7 +7229,7 @@ class Meta: # models.Index(fields=['name'], name='WorkbenchNameIDX') ] - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Workbenchdataitem(models.Model): @@ -7344,7 +7342,7 @@ class Meta: db_table = 'workbenchrowexportedrelationship' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Workbenchrowimage(models.Model): @@ -7420,7 +7418,7 @@ class Meta: db_table = 'workbenchtemplate' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class Workbenchtemplatemappingitem(models.Model): @@ -7460,5 +7458,5 @@ class Meta: db_table = 'workbenchtemplatemappingitem' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) diff --git a/specifyweb/specify/tree_extras.py b/specifyweb/specify/tree_extras.py index 1658997eccc..f86db7de5e2 100644 --- a/specifyweb/specify/tree_extras.py +++ b/specifyweb/specify/tree_extras.py @@ -27,7 +27,7 @@ def validate_node_numbers(table, revalidate_after=True): if revalidate_after: validate_tree_numbering(table) -class Tree(models.Model): # FUTURE: class Tree(SpTimestampedModel): +class Tree(models.Model): class Meta: abstract = True diff --git a/specifyweb/workbench/models.py b/specifyweb/workbench/models.py index c055f0702aa..c74ea3f1f75 100644 --- a/specifyweb/workbench/models.py +++ b/specifyweb/workbench/models.py @@ -1,6 +1,5 @@ import json from functools import partialmethod -from model_utils import FieldTracker from django import http from django.core.exceptions import ObjectDoesNotExist @@ -100,7 +99,6 @@ class Spdataset(Dataset): class Meta: db_table = 'spdataset' - # timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) # save = partialmethod(custom_save) def get_dataset_as_dict(self): From ccd714ec88454a7d1a12ee3b9e70933d9459575a Mon Sep 17 00:00:00 2001 From: realVinayak Date: Mon, 22 Jul 2024 12:46:50 -0500 Subject: [PATCH 13/27] Use custom save logic --- specifyweb/specify/api.py | 26 +++----- specifyweb/specify/build_models.py | 8 +-- specifyweb/specify/model_extras.py | 5 +- specifyweb/specify/model_timestamp.py | 37 ++++++++--- specifyweb/specify/models.py | 5 +- specifyweb/specify/tests/test_api.py | 73 +-------------------- specifyweb/specify/tests/test_timestamps.py | 48 ++++++++++++++ specifyweb/specify/tree_extras.py | 5 +- 8 files changed, 93 insertions(+), 114 deletions(-) create mode 100644 specifyweb/specify/tests/test_timestamps.py diff --git a/specifyweb/specify/api.py b/specifyweb/specify/api.py index 4dfd4d4b88a..9008ddb4ba7 100644 --- a/specifyweb/specify/api.py +++ b/specifyweb/specify/api.py @@ -368,6 +368,10 @@ def set_field_if_exists(obj, field: str, value) -> None: if f.concrete: setattr(obj, field, value) +def _maybe_delete(data: Dict[str, Any], to_delete: str): + if to_delete in data: + del data[to_delete] + def cleanData(model, data: Dict[str, Any], agent) -> Dict[str, Any]: """Returns a copy of data with only fields that are part of model, removing metadata fields and warning on unexpected extra fields.""" @@ -400,30 +404,18 @@ def cleanData(model, data: Dict[str, Any], agent) -> Dict[str, Any]: if model is models.Agent: # setting user agents is part of the user management system. - try: - del cleaned['specifyuser'] - except KeyError: - pass + _maybe_delete(cleaned, 'specifyuser') # guid should only be updatable for taxon and geography if model not in (models.Taxon, models.Geography): - try: - del cleaned['guid'] - except KeyError: - pass + _maybe_delete(cleaned, 'guid') # timestampcreated should never be updated. - # ... well it is now ¯\_(ツ)_/¯ - # New requirments are for timestampcreated to be overridable. - try: - # del cleaned['timestampcreated'] - pass - except KeyError: - pass + # _maybe_delete(cleaned, 'timestampcreated') # Password should be set though the /api/set_password// endpoint - if model is models.Specifyuser and 'password' in cleaned: - del cleaned['password'] + if model is models.Specifyuser: + _maybe_delete(cleaned, 'password') return cleaned diff --git a/specifyweb/specify/build_models.py b/specifyweb/specify/build_models.py index 60c5aff7f78..0044585dec2 100644 --- a/specifyweb/specify/build_models.py +++ b/specifyweb/specify/build_models.py @@ -3,7 +3,7 @@ from specifyweb.businessrules.exceptions import AbortSave from . import model_extras -from .model_timestamp import pre_save_auto_timestamp_field_with_override, timestamp_fields +from .model_timestamp import save_auto_timestamp_field_with_override appname = __name__.split('.')[-2] @@ -59,13 +59,9 @@ class Meta: if 'rankid' in attrs: ordering += ('rankid', ) - has_timestamp_fields = any(field.lower() in timestamp_fields for field in table.fields) - def save(self, *args, **kwargs): try: - if has_timestamp_fields: - pre_save_auto_timestamp_field_with_override(self) - return super(model, self).save(*args, **kwargs) + return save_auto_timestamp_field_with_override(super(model, self).save, args, kwargs, self) except AbortSave: return diff --git a/specifyweb/specify/model_extras.py b/specifyweb/specify/model_extras.py index 017e7f1462d..3e505a70247 100644 --- a/specifyweb/specify/model_extras.py +++ b/specifyweb/specify/model_extras.py @@ -5,7 +5,7 @@ from django.conf import settings from django.utils import timezone -from .model_timestamp import pre_save_auto_timestamp_field_with_override +from .model_timestamp import save_auto_timestamp_field_with_override from .tree_extras import Tree, TreeRank if settings.AUTH_LDAP_SERVER_URI is not None: @@ -117,8 +117,7 @@ def save(self, *args, **kwargs): if self.id and self.usertype != 'Manager': self.clear_admin() - pre_save_auto_timestamp_field_with_override(self) - return super(Specifyuser, self).save(*args, **kwargs) + return save_auto_timestamp_field_with_override(super(Specifyuser, self).save, args, kwargs, self) class Meta: abstract = True diff --git a/specifyweb/specify/model_timestamp.py b/specifyweb/specify/model_timestamp.py index 55d064dfd24..0080174df18 100644 --- a/specifyweb/specify/model_timestamp.py +++ b/specifyweb/specify/model_timestamp.py @@ -1,18 +1,35 @@ -from django.db import models from django.utils import timezone -from django.conf import settings +from django.db.models import Model +timestamp_fields = [('timestampmodified', True), ('timestampcreated', False)] +fields_to_skip = [field[0] for field in timestamp_fields if not field[1]] -timestamp_fields = {'timestampmodified', 'timestampcreated'} - -def pre_save_auto_timestamp_field_with_override(obj): +def save_auto_timestamp_field_with_override(save_func, args, kwargs, obj): # If object already is present, reset timestamps to null. + model: Model = obj.__class__ + is_forced_insert = kwargs.get('force_insert', False) + fields_to_update = kwargs.get('update_fields', None) + if fields_to_update is None: + fields_to_update = [ + field.name for field in model._meta.get_fields(include_hidden=True) if field.concrete + and not field.primary_key + ] + + if obj.id is not None: + fields_to_update = [ + field for field in fields_to_update + if field not in fields_to_skip + ] + + current = timezone.now() + _set_if_empty(obj, timestamp_fields, current, obj.pk is not None) + new_kwargs = {**kwargs, 'update_fields': fields_to_update} if obj.pk is not None and not is_forced_insert else kwargs + return save_func(*args, **new_kwargs) - if obj.id is None: - return - - for field in timestamp_fields: +def _set_if_empty(obj, fields, default_value, override=False): + for field, can_override in fields: if not hasattr(obj, field): continue - setattr(obj, field, None) \ No newline at end of file + if (override and can_override) or getattr(obj, field) is None: + setattr(obj, field, default_value) \ No newline at end of file diff --git a/specifyweb/specify/models.py b/specifyweb/specify/models.py index 85a84322848..664407505f0 100644 --- a/specifyweb/specify/models.py +++ b/specifyweb/specify/models.py @@ -2,7 +2,7 @@ from django.db import models from django.utils import timezone from specifyweb.businessrules.exceptions import AbortSave -from specifyweb.specify.model_timestamp import pre_save_auto_timestamp_field_with_override +from specifyweb.specify.model_timestamp import save_auto_timestamp_field_with_override from specifyweb.specify import model_extras from .datamodel import datamodel import logging @@ -18,8 +18,7 @@ def protect_with_blockers(collector, field, sub_objs, using): def custom_save(self, *args, **kwargs): try: # Custom save logic here, if necessary - pre_save_auto_timestamp_field_with_override(self) - super(self.__class__, self).save(*args, **kwargs) + save_auto_timestamp_field_with_override(super(self.__class__, self).save, args, kwargs, self) except AbortSave as e: # Handle AbortSave exception as needed logger.error("Save operation aborted: %s", e) diff --git a/specifyweb/specify/tests/test_api.py b/specifyweb/specify/tests/test_api.py index b7568983f42..bdec47e896d 100644 --- a/specifyweb/specify/tests/test_api.py +++ b/specifyweb/specify/tests/test_api.py @@ -17,7 +17,6 @@ def get_table(name: str): return getattr(models, name.capitalize()) - class MainSetupTearDown: def setUp(self): disconnect_signal('pre_save', None, dispatch_uid=UNIQUENESS_DISPATCH_UID) @@ -94,6 +93,7 @@ class ApiTests(MainSetupTearDown, TestCase): pass skip_perms_check = lambda x: None class SimpleApiTests(ApiTests): + def test_get_collection(self): data = api.get_collection(self.collection, 'collectionobject', skip_perms_check) self.assertEqual(data['meta']['total_count'], len(self.collectionobjects)) @@ -132,77 +132,6 @@ def test_delete_object(self): api.delete_resource(self.collection, self.agent, 'collectionobject', obj.id, obj.version) self.assertEqual(models.Collectionobject.objects.filter(id=obj.id).count(), 0) - def test_timestamp_override_object(self): - manual_datetime_1 = datetime(1960, 1, 1, 0, 0, 0) - manual_datetime_2 = datetime(2020, 1, 1, 0, 0, 0) - cur_time = datetime.now() - - def timestamp_field_assert(obj, manual_datetime): - self.assertEqual(obj.timestampcreated, manual_datetime) - self.assertEqual(obj.timestampmodified, manual_datetime) - - # Test with api.create_obj - obj = api.create_obj(self.collection, self.agent, 'collectionobject', { - 'collection': api.uri_for_model('collection', self.collection.id), - 'catalognumber': 'foobar', - 'timestampcreated': manual_datetime_1, 'timestampmodified': manual_datetime_1}) - obj = models.Collectionobject.objects.get(id=obj.id) - timestamp_field_assert(obj, manual_datetime_1) - - # Test editing obj after creating with api.create_obj - data = api.get_resource('collection', self.collection.id, skip_perms_check) - data['timestampcreated'] = manual_datetime_2 - data['timestampmodified'] = manual_datetime_2 - api.update_obj(self.collection, self.agent, 'collection', - data['id'], data['version'], data) - obj = models.Collection.objects.get(id=self.collection.id) - timestamp_field_assert(obj, manual_datetime_2) - - # Test with direct object creation - CollectionObject = getattr(models, 'Collectionobject') - obj = CollectionObject( - timestampcreated=manual_datetime_1, - timestampmodified=manual_datetime_1, - collectionmemberid=1, - collection=self.collection) - obj.save() - timestamp_field_assert(obj, manual_datetime_1) - - # Test editing obj after creating with direct object creation - CollectionObject = getattr(models, 'Collectionobject') - obj = CollectionObject.objects.create( - timestampcreated=manual_datetime_2, - timestampmodified=manual_datetime_2, - collectionmemberid=1, - collection=self.collection) - obj.save() - timestamp_field_assert(obj, manual_datetime_2) - - # Test with objects.create - CollectionObject = getattr(models, 'Collectionobject') - obj = CollectionObject.objects.create( - timestampcreated=manual_datetime_1, - timestampmodified=manual_datetime_1, - collectionmemberid=1, - collection=self.collection) - obj.save() - timestamp_field_assert(obj, manual_datetime_1) - - # Test editing obj after creating with objects.create - obj.timestampcreated = manual_datetime_2 - obj.timestampmodified = manual_datetime_2 - obj.save() - timestamp_field_assert(obj, manual_datetime_2) - - # Test with current time - CollectionObject = getattr(models, 'Collectionobject') - obj = CollectionObject.objects.create( - collectionmemberid=1, - collection=self.collection) - obj.save() - self.assertGreaterEqual(obj.timestampcreated, cur_time) - self.assertGreaterEqual(obj.timestampmodified, cur_time) - class RecordSetTests(ApiTests): def setUp(self): super(RecordSetTests, self).setUp() diff --git a/specifyweb/specify/tests/test_timestamps.py b/specifyweb/specify/tests/test_timestamps.py new file mode 100644 index 00000000000..da5584af039 --- /dev/null +++ b/specifyweb/specify/tests/test_timestamps.py @@ -0,0 +1,48 @@ +""" +Tests for timestamps additional logic +""" +from datetime import datetime +from django.utils import timezone +from specifyweb.specify.tests.test_api import ApiTests, skip_perms_check +from specifyweb.specify import api +from specifyweb.specify.models import Collectionobject + +class TimeStampTests(ApiTests): + + def test_blank_timestamps(self): + cur_time = timezone.now() + + obj = Collectionobject.objects.create( + collectionmemberid=1, + collection=self.collection) + + self.assertGreaterEqual(obj.timestampcreated, cur_time) + self.assertGreaterEqual(obj.timestampmodified, cur_time) + + def test_can_override_new_timestamps_api(self): + datetime_1 = datetime(1960, 1, 1, 0, 0, 0) + datetime_2 = datetime(2020, 1, 1, 0, 0, 0) + + obj = api.create_obj(self.collection, self.agent, 'collectionobject', { + 'collection': api.uri_for_model('collection', self.collection.id), + 'catalognumber': 'foobar', + 'timestampcreated': datetime_1, 'timestampmodified': datetime_2}) + + self.assertEqual(datetime_1, obj.timestampcreated) + self.assertEqual(datetime_2, obj.timestampmodified) + + def test_cannot_override_old_timestamps_api(self): + datetime_1 = datetime(1960, 1, 1, 0, 0, 0) + datetime_2 = datetime(2020, 1, 1, 0, 0, 0) + current = timezone.now() + co_to_edit = self.collectionobjects[0] + data = api.get_resource('collectionobject', co_to_edit.id, skip_perms_check) + data['timestampcreated'] = datetime_1 + data['timestampmodified'] = datetime_2 + obj = api.update_obj(self.collection, self.agent, 'collectionobject', data['id'], data['version'], data) + + obj.refresh_from_db() + self.assertNotEqual(obj.timestampcreated, datetime_1, "Was able to override!") + self.assertNotEqual(obj.timestampmodified, datetime_2, "Was able to override!") + self.assertGreaterEqual(obj.timestampmodified, current, "Timestampmodified did not update correctly!") + self.assertGreater(current, obj.timestampcreated, "Timestampcreated should be at the past for this record!") \ No newline at end of file diff --git a/specifyweb/specify/tree_extras.py b/specifyweb/specify/tree_extras.py index f86db7de5e2..1a8296c44a2 100644 --- a/specifyweb/specify/tree_extras.py +++ b/specifyweb/specify/tree_extras.py @@ -4,7 +4,7 @@ from specifyweb.specify.tree_ranks import RankOperation, post_tree_rank_save, pre_tree_rank_deletion, \ verify_rank_parent_chain_integrity, pre_tree_rank_init, post_tree_rank_deletion -from specifyweb.specify.model_timestamp import pre_save_auto_timestamp_field_with_override +from specifyweb.specify.model_timestamp import save_auto_timestamp_field_with_override logger = logging.getLogger(__name__) @@ -33,8 +33,7 @@ class Meta: def save(self, *args, skip_tree_extras=False, **kwargs): def save(): - pre_save_auto_timestamp_field_with_override(self) - super(Tree, self).save(*args, **kwargs) + save_auto_timestamp_field_with_override(super(Tree, self).save, args, kwargs, self) if skip_tree_extras: return save() From 256d3c9e503e3269612833c146eaa6edbd6f9d66 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Mon, 22 Jul 2024 14:49:49 -0500 Subject: [PATCH 14/27] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b9a1c86cf5..0f130c3222c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [7.9.6.2](https://github.com/specify/specify7/compare/v7.9.6.1...v7.9.6.2) (22 July 2024) + +- Fixed an issue that prevented `TimestampModified` from being captured upon saving a record since the `v7.9.6` release ([#5108](https://github.com/specify/specify7/issues/5108) – *Reported by the University of Kansas and Ohio State University*) +- Fixed an issue that caused large trees to perform slowly or crash the browser due to using too much memory ([#5115](https://github.com/specify/specify7/pull/5115) – *Reported by The Hebrew University of Jerusalem and Royal Botanic Gardens Edinburgh*) + ## [7.9.6.1](https://github.com/specify/specify7/compare/v7.9.6...v7.9.6.1) (9 July 2024) - Fixes an issue that led to tree definition item separators being trimmed ([#5076](https://github.com/specify/specify7/pull/5076)) From b8cc45b0e14dd2cbf48cdf5cda66e2677610fc98 Mon Sep 17 00:00:00 2001 From: realVinayak Date: Tue, 23 Jul 2024 12:45:08 -0500 Subject: [PATCH 15/27] Remove fieldtracker instances --- specifyweb/specify/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/specifyweb/specify/models.py b/specifyweb/specify/models.py index db82cd3bad9..c9999e14435 100644 --- a/specifyweb/specify/models.py +++ b/specifyweb/specify/models.py @@ -7511,7 +7511,6 @@ class Meta: db_table = 'collectionobjecttype' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) save = partialmethod(custom_save) class CollectionObjectGroupType(models.Model): @@ -7576,7 +7575,6 @@ class Meta: db_table = 'collectionobjectgroup' ordering = () - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) save = partialmethod(custom_save) class CollectionObjectGroupJoin(models.Model): # aka. CoJo or CogJoin @@ -7613,5 +7611,4 @@ class Meta: db_table = 'collectionobjectgroupjoin' ordering = () - timestamptracker = FieldTracker(fields=["timestampcreated", "timestampmodified"]) save = partialmethod(custom_save) From a35d9979149351906e5f5325ccd798657172e3f4 Mon Sep 17 00:00:00 2001 From: realVinayak Date: Tue, 23 Jul 2024 12:45:46 -0500 Subject: [PATCH 16/27] Remove unused utility --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index be0481f61d5..d85e6e1ba9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,3 @@ PyJWT==2.3.0 django-auth-ldap==1.2.15 jsonschema==3.2.0 typing-extensions==4.3.0 -django-model-utils==4.4.0 From 8690c8980650ee7c2970d3911fca13bbb18b3cb2 Mon Sep 17 00:00:00 2001 From: realVinayak Date: Tue, 23 Jul 2024 12:49:54 -0500 Subject: [PATCH 17/27] Removed FieldTracker instance --- specifyweb/specify/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/specifyweb/specify/models.py b/specifyweb/specify/models.py index c9999e14435..a6c71c6ab08 100644 --- a/specifyweb/specify/models.py +++ b/specifyweb/specify/models.py @@ -7534,8 +7534,7 @@ class CollectionObjectGroupType(models.Model): class Meta: db_table = 'collectionobjectgrouptype' ordering = () - - timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified']) + save = partialmethod(custom_save) class CollectionObjectGroup(models.Model): # aka. Cog From dea079e51f0ae578ea8fdbeb80b0c0c07c0de4fb Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:40:23 -0700 Subject: [PATCH 18/27] Display system tables by default in query list menu option Fixes #5128 --- .../js_src/lib/components/DataEntryTables/Edit.tsx | 1 + .../js_src/lib/components/SchemaConfig/Tables.tsx | 8 ++++++-- .../lib/components/Toolbar/QueryTablesEdit.tsx | 12 +++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataEntryTables/Edit.tsx b/specifyweb/frontend/js_src/lib/components/DataEntryTables/Edit.tsx index 663d9dc27b4..41910f95a36 100644 --- a/specifyweb/frontend/js_src/lib/components/DataEntryTables/Edit.tsx +++ b/specifyweb/frontend/js_src/lib/components/DataEntryTables/Edit.tsx @@ -130,6 +130,7 @@ function CustomEditTables({ : formsText.configureInteractionTables() } isNoRestrictionMode={false} + parent="DataEntryList" tables={tables} onChange={handleChange} onClose={handleClose} diff --git a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx index 02f493df7cf..657bacd4790 100644 --- a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx +++ b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx @@ -82,11 +82,15 @@ export function tablesFilter( showAdvancedTables: boolean, { name, overrides }: SpecifyTable, // Don't exclude a table if user already has it selected - selectedTables: RA | undefined = undefined + selectedTables: RA | undefined = undefined, + parent?: 'DataEntryList' | 'QueryList' ): boolean { if (selectedTables?.includes(name) === true) return true; - const isRestricted = overrides.isHidden || overrides.isSystem; + const isRestricted = + parent === 'QueryList' + ? overrides.isHidden + : overrides.isHidden || overrides.isSystem; if (!showHiddenTables && isRestricted) return false; const hasAccess = hasTablePermission(name, 'read'); if (!showNoAccessTables && !hasAccess) return false; diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx index 2278b09a498..ba27634eb4e 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx @@ -31,6 +31,7 @@ export function QueryTablesEdit({ defaultTables={defaultQueryTablesConfig} header={queryText.configureQueryTables()} isNoRestrictionMode={isNoRestrictionMode} + parent="QueryList" tables={tables} onChange={setTables} onClose={handleClose} @@ -51,6 +52,7 @@ export function TablesListEdit({ tables: selectedTables, onChange: handleChange, onClose: handleClose, + parent, }: { readonly isNoRestrictionMode: boolean; readonly defaultTables: RA; @@ -58,11 +60,19 @@ export function TablesListEdit({ readonly tables: RA; readonly onChange: (table: RA) => void; readonly onClose: () => void; + readonly parent: 'DataEntryList' | 'QueryList'; }): JSX.Element { const selectedValues = selectedTables.map(({ name }) => name); const allTables = Object.values(genericTables) .filter((table) => - tablesFilter(isNoRestrictionMode, false, true, table, selectedValues) + tablesFilter( + isNoRestrictionMode, + false, + true, + table, + selectedValues, + parent + ) ) // TODO: temp fix, remove this, use to hide geo tables for COG until 9.8 release .filter((table) => !HIDDEN_GEO_TABLES.has(table.name)) From c1e6cc207d92b29de330d73bd74982ebe94a504d Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:45:43 -0700 Subject: [PATCH 19/27] Simplify --- .../frontend/js_src/lib/components/DataEntryTables/Edit.tsx | 1 - .../frontend/js_src/lib/components/SchemaConfig/Tables.tsx | 2 +- .../frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataEntryTables/Edit.tsx b/specifyweb/frontend/js_src/lib/components/DataEntryTables/Edit.tsx index 41910f95a36..663d9dc27b4 100644 --- a/specifyweb/frontend/js_src/lib/components/DataEntryTables/Edit.tsx +++ b/specifyweb/frontend/js_src/lib/components/DataEntryTables/Edit.tsx @@ -130,7 +130,6 @@ function CustomEditTables({ : formsText.configureInteractionTables() } isNoRestrictionMode={false} - parent="DataEntryList" tables={tables} onChange={handleChange} onClose={handleClose} diff --git a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx index 657bacd4790..29e8fbf2bc6 100644 --- a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx +++ b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx @@ -83,7 +83,7 @@ export function tablesFilter( { name, overrides }: SpecifyTable, // Don't exclude a table if user already has it selected selectedTables: RA | undefined = undefined, - parent?: 'DataEntryList' | 'QueryList' + parent?: 'QueryList' ): boolean { if (selectedTables?.includes(name) === true) return true; diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx index ba27634eb4e..3546e045c01 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx @@ -60,7 +60,7 @@ export function TablesListEdit({ readonly tables: RA; readonly onChange: (table: RA) => void; readonly onClose: () => void; - readonly parent: 'DataEntryList' | 'QueryList'; + readonly parent?: 'QueryList'; }): JSX.Element { const selectedValues = selectedTables.map(({ name }) => name); const allTables = Object.values(genericTables) From 3783b6abe6d7852caa471543f98a7a5fe877dcb8 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 24 Jul 2024 08:27:00 -0700 Subject: [PATCH 20/27] Change 4 tables to not be system --- .../lib/components/DataModel/schemaOverrides.ts | 3 --- .../js_src/lib/components/SchemaConfig/Tables.tsx | 8 ++------ .../lib/components/Toolbar/QueryTablesEdit.tsx | 12 +----------- specifyweb/specify/datamodel.py | 2 +- 4 files changed, 4 insertions(+), 21 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/schemaOverrides.ts b/specifyweb/frontend/js_src/lib/components/DataModel/schemaOverrides.ts index 27400e4fb62..c009e09fad1 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/schemaOverrides.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/schemaOverrides.ts @@ -68,9 +68,6 @@ const tableOverwrites: Partial> = { CollectingEventAttr: 'system', CollectionObjectAttr: 'system', LatLonPolygonPnt: 'system', - Collection: 'system', - Discipline: 'system', - Division: 'system', Institution: 'system', Workbench: 'hidden', WorkbenchDataItem: 'hidden', diff --git a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx index 29e8fbf2bc6..02f493df7cf 100644 --- a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx +++ b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx @@ -82,15 +82,11 @@ export function tablesFilter( showAdvancedTables: boolean, { name, overrides }: SpecifyTable, // Don't exclude a table if user already has it selected - selectedTables: RA | undefined = undefined, - parent?: 'QueryList' + selectedTables: RA | undefined = undefined ): boolean { if (selectedTables?.includes(name) === true) return true; - const isRestricted = - parent === 'QueryList' - ? overrides.isHidden - : overrides.isHidden || overrides.isSystem; + const isRestricted = overrides.isHidden || overrides.isSystem; if (!showHiddenTables && isRestricted) return false; const hasAccess = hasTablePermission(name, 'read'); if (!showNoAccessTables && !hasAccess) return false; diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx index 3546e045c01..2278b09a498 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx @@ -31,7 +31,6 @@ export function QueryTablesEdit({ defaultTables={defaultQueryTablesConfig} header={queryText.configureQueryTables()} isNoRestrictionMode={isNoRestrictionMode} - parent="QueryList" tables={tables} onChange={setTables} onClose={handleClose} @@ -52,7 +51,6 @@ export function TablesListEdit({ tables: selectedTables, onChange: handleChange, onClose: handleClose, - parent, }: { readonly isNoRestrictionMode: boolean; readonly defaultTables: RA; @@ -60,19 +58,11 @@ export function TablesListEdit({ readonly tables: RA; readonly onChange: (table: RA) => void; readonly onClose: () => void; - readonly parent?: 'QueryList'; }): JSX.Element { const selectedValues = selectedTables.map(({ name }) => name); const allTables = Object.values(genericTables) .filter((table) => - tablesFilter( - isNoRestrictionMode, - false, - true, - table, - selectedValues, - parent - ) + tablesFilter(isNoRestrictionMode, false, true, table, selectedValues) ) // TODO: temp fix, remove this, use to hide geo tables for COG until 9.8 release .filter((table) => !HIDDEN_GEO_TABLES.has(table.name)) diff --git a/specifyweb/specify/datamodel.py b/specifyweb/specify/datamodel.py index 579a588003e..8d0e599a9b8 100644 --- a/specifyweb/specify/datamodel.py +++ b/specifyweb/specify/datamodel.py @@ -6192,7 +6192,7 @@ classname='edu.ku.brc.specify.datamodel.SpAuditLog', table='spauditlog', tableId=530, - system=True, + system=False, idColumn='SpAuditLogID', idFieldName='spAuditLogId', idField=IdField(name='spAuditLogId', column='SpAuditLogID', type='java.lang.Integer'), From 8feb072a53dbdc912adde6cb8a1e8fd2f780aae5 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 24 Jul 2024 09:05:46 -0700 Subject: [PATCH 21/27] Test queryListTables with variable --- .../js_src/lib/components/DataModel/schemaOverrides.ts | 3 +++ .../frontend/js_src/lib/components/SchemaConfig/Tables.tsx | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/schemaOverrides.ts b/specifyweb/frontend/js_src/lib/components/DataModel/schemaOverrides.ts index c009e09fad1..27400e4fb62 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/schemaOverrides.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/schemaOverrides.ts @@ -68,6 +68,9 @@ const tableOverwrites: Partial> = { CollectingEventAttr: 'system', CollectionObjectAttr: 'system', LatLonPolygonPnt: 'system', + Collection: 'system', + Discipline: 'system', + Division: 'system', Institution: 'system', Workbench: 'hidden', WorkbenchDataItem: 'hidden', diff --git a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx index 02f493df7cf..d2e14453cc9 100644 --- a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx +++ b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx @@ -72,6 +72,11 @@ type CacheKey = { : never; }[keyof CacheDefinitions]; +const tablesToIncludeInQueryList = new Set([ + 'Collection', + 'Discipline', + 'Division', +]); /** * Get a function for trimming down all tables to list of tables * user is expected to commonly access @@ -85,6 +90,7 @@ export function tablesFilter( selectedTables: RA | undefined = undefined ): boolean { if (selectedTables?.includes(name) === true) return true; + if (tablesToIncludeInQueryList.has(name)) return true; const isRestricted = overrides.isHidden || overrides.isSystem; if (!showHiddenTables && isRestricted) return false; From 0fe5c2c7aeaadd9f1b37a020fd7520595863e9c5 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Thu, 25 Jul 2024 07:33:07 -0700 Subject: [PATCH 22/27] Filter tables depending on user status --- .../js_src/lib/components/SchemaConfig/Tables.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx index d2e14453cc9..02551310c4f 100644 --- a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx +++ b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx @@ -16,6 +16,7 @@ import { Link } from '../Atoms/Link'; import type { SpecifyTable } from '../DataModel/specifyTable'; import { genericTables } from '../DataModel/tables'; import type { Tables } from '../DataModel/types'; +import { userInformation } from '../InitialContext/userInformation'; import { Dialog } from '../Molecules/Dialog'; import { TableIcon } from '../Molecules/TableIcon'; import { hasTablePermission } from '../Permissions/helpers'; @@ -72,11 +73,6 @@ type CacheKey = { : never; }[keyof CacheDefinitions]; -const tablesToIncludeInQueryList = new Set([ - 'Collection', - 'Discipline', - 'Division', -]); /** * Get a function for trimming down all tables to list of tables * user is expected to commonly access @@ -90,9 +86,12 @@ export function tablesFilter( selectedTables: RA | undefined = undefined ): boolean { if (selectedTables?.includes(name) === true) return true; - if (tablesToIncludeInQueryList.has(name)) return true; + const userIsAdmin = + userInformation.isadmin && userInformation.usertype === 'Manager'; - const isRestricted = overrides.isHidden || overrides.isSystem; + const isRestricted = userIsAdmin + ? overrides.isHidden + : overrides.isHidden || overrides.isSystem; if (!showHiddenTables && isRestricted) return false; const hasAccess = hasTablePermission(name, 'read'); if (!showNoAccessTables && !hasAccess) return false; From 301fac6f7a070cefbe9c5a6bdf2804b485fe3623 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Thu, 25 Jul 2024 09:12:25 -0700 Subject: [PATCH 23/27] Remove spauditlog from sp6 system tables --- specifyweb/specify/load_datamodel.py | 1 - 1 file changed, 1 deletion(-) diff --git a/specifyweb/specify/load_datamodel.py b/specifyweb/specify/load_datamodel.py index 7382c08de78..88280c9d578 100644 --- a/specifyweb/specify/load_datamodel.py +++ b/specifyweb/specify/load_datamodel.py @@ -481,7 +481,6 @@ def flag_system_tables(datamodel: Datamodel) -> None: 'Spappresource', 'Spappresourcedata', 'Spappresourcedir', - 'Spauditlog', 'Spauditlogfield', 'Spexportschema', 'Spexportschemaitem', From cfaf4034d6cbd65cdfcbc8de7a90708af46ebb57 Mon Sep 17 00:00:00 2001 From: Max Patiiuk Date: Thu, 25 Jul 2024 18:38:18 -0700 Subject: [PATCH 24/27] chore: reduce console noise --- specifyweb/frontend/js_src/css/main.css | 1 - .../js_src/lib/components/Core/Entrypoint.tsx | 3 +- .../lib/components/Errors/interceptLogs.ts | 7 +++ .../lib/components/InitialContext/index.ts | 18 ------- .../lib/components/Permissions/definitions.ts | 50 +++++++++---------- .../lib/components/Permissions/index.ts | 4 +- 6 files changed, 36 insertions(+), 47 deletions(-) diff --git a/specifyweb/frontend/js_src/css/main.css b/specifyweb/frontend/js_src/css/main.css index ce1f42ddb8d..efc7ff4a690 100644 --- a/specifyweb/frontend/js_src/css/main.css +++ b/specifyweb/frontend/js_src/css/main.css @@ -150,7 +150,6 @@ /* 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; } diff --git a/specifyweb/frontend/js_src/lib/components/Core/Entrypoint.tsx b/specifyweb/frontend/js_src/lib/components/Core/Entrypoint.tsx index 575b5eba5e3..075f0fe56a8 100644 --- a/specifyweb/frontend/js_src/lib/components/Core/Entrypoint.tsx +++ b/specifyweb/frontend/js_src/lib/components/Core/Entrypoint.tsx @@ -16,8 +16,8 @@ function entrypoint(): void { interceptLogs(); - console.group('Specify App Starting'); if (process.env.NODE_ENV === 'production') { + console.group('Specify App Starting'); console.log( '%cDocumentation for Developers:\n', 'font-weight: bold', @@ -27,7 +27,6 @@ function entrypoint(): void { const entrypointName = parseDjangoDump>('entrypoint-name') ?? 'main'; - console.log(entrypointName); unlockInitialContext(entrypointName); globalThis.addEventListener?.('load', () => { diff --git a/specifyweb/frontend/js_src/lib/components/Errors/interceptLogs.ts b/specifyweb/frontend/js_src/lib/components/Errors/interceptLogs.ts index 9bddd60b955..a6121e5fa25 100644 --- a/specifyweb/frontend/js_src/lib/components/Errors/interceptLogs.ts +++ b/specifyweb/frontend/js_src/lib/components/Errors/interceptLogs.ts @@ -117,6 +117,13 @@ export function interceptLogs(): void { const context = getLogContext(); const hasContext = Object.keys(context).length > 0; + // Silencing https://github.com/reactjs/react-modal/issues/808 + if ( + args[0] === + "React-Modal: Cannot register modal instance that's already open" + ) + return; + /** * If actively redirecting log output, don't print to console * (printing object to console prevents garbage collection diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/index.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/index.ts index 6f3bf76163a..c4fb91e22d5 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/index.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/index.ts @@ -3,10 +3,7 @@ */ import type { MimeType } from '../../utils/ajax'; -import { f } from '../../utils/functools'; import { defined } from '../../utils/types'; -import { formatNumber } from '../Atoms/Internationalization'; -import { SECOND } from '../Atoms/timeUnits'; /** * This belongs to ./components/toolbar/cachebuster.tsx but was moved here @@ -56,7 +53,6 @@ export const unlockInitialContext = (entrypoint: typeof entrypointName): void => export const load = async (path: string, mimeType: MimeType): Promise => contextUnlockedPromise.then(async (entrypoint) => { if (entrypoint !== 'main') return foreverFetch(); - const startTime = Date.now(); // Doing async import to avoid a circular dependency const { ajax } = await import('../../utils/ajax'); @@ -65,21 +61,7 @@ export const load = async (path: string, mimeType: MimeType): Promise => errorMode: 'visible', headers: { Accept: mimeType }, }); - const endTime = Date.now(); - const timePassed = endTime - startTime; - // A very crude detection mechanism - const isCached = timePassed < 100; - // So as not to spam the tests - if (process.env.NODE_ENV !== 'test') - console.log( - `${path} %c[${ - isCached - ? 'cached' - : `${formatNumber(f.round(timePassed / SECOND, 0.01))}s` - }]`, - `color: ${isCached ? '#9fa' : '#f99'}` - ); return data; }); diff --git a/specifyweb/frontend/js_src/lib/components/Permissions/definitions.ts b/specifyweb/frontend/js_src/lib/components/Permissions/definitions.ts index f8b056eea95..24db5e23b60 100644 --- a/specifyweb/frontend/js_src/lib/components/Permissions/definitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Permissions/definitions.ts @@ -1,20 +1,24 @@ export const tableActions = ['read', 'create', 'update', 'delete'] as const; export const collectionAccessResource = '/system/sp7/collection'; export const operationPolicies = { - '/system/sp7/collection': ['access'], - '/admin/user/password': ['update'], '/admin/user/agents': ['update'], - '/admin/user/sp6/is_admin': ['update'], - '/record/merge': ['update', 'delete'], '/admin/user/invite_link': ['create'], '/admin/user/oic_providers': ['read'], + '/admin/user/password': ['update'], '/admin/user/sp6/collection_access': ['read', 'update'], - '/report': ['execute'], + '/admin/user/sp6/is_admin': ['update'], + '/attachment_import/dataset': [ + 'create', + 'update', + 'delete', + 'upload', + 'rollback', + ], '/export/dwca': ['execute'], '/export/feed': ['force_update'], + '/permissions/library/roles': ['read', 'create', 'update', 'delete'], '/permissions/list_admins': ['read'], '/permissions/policies/user': ['read', 'update'], - '/permissions/user/roles': ['read', 'update'], '/permissions/roles': [ 'read', 'create', @@ -22,8 +26,16 @@ export const operationPolicies = { 'delete', 'copy_from_library', ], - '/permissions/library/roles': ['read', 'create', 'update', 'delete'], - '/tree/edit/taxon': ['merge', 'move', 'synonymize', 'desynonymize', 'repair'], + '/permissions/user/roles': ['read', 'update'], + '/querybuilder/query': [ + 'execute', + 'export_csv', + 'export_kml', + 'create_recordset', + ], + '/record/merge': ['update', 'delete'], + '/report': ['execute'], + '/system/sp7/collection': ['access'], '/tree/edit/geography': [ 'merge', 'move', @@ -31,34 +43,29 @@ export const operationPolicies = { 'desynonymize', 'repair', ], - '/tree/edit/storage': [ + '/tree/edit/geologictimeperiod': [ 'merge', 'move', 'synonymize', 'desynonymize', 'repair', - 'bulk_move', ], - '/tree/edit/geologictimeperiod': [ + '/tree/edit/lithostrat': [ 'merge', 'move', 'synonymize', 'desynonymize', 'repair', ], - '/tree/edit/lithostrat': [ + '/tree/edit/storage': [ 'merge', 'move', + 'bulk_move', 'synonymize', 'desynonymize', 'repair', ], - '/querybuilder/query': [ - 'execute', - 'export_csv', - 'export_kml', - 'create_recordset', - ], + '/tree/edit/taxon': ['merge', 'move', 'synonymize', 'desynonymize', 'repair'], '/workbench/dataset': [ 'create', 'update', @@ -69,13 +76,6 @@ export const operationPolicies = { 'transfer', 'create_recordset', ], - '/attachment_import/dataset': [ - 'create', - 'update', - 'delete', - 'upload', - 'rollback', - ], } as const; /** diff --git a/specifyweb/frontend/js_src/lib/components/Permissions/index.ts b/specifyweb/frontend/js_src/lib/components/Permissions/index.ts index f3b3c415eca..4b17cc6eda9 100644 --- a/specifyweb/frontend/js_src/lib/components/Permissions/index.ts +++ b/specifyweb/frontend/js_src/lib/components/Permissions/index.ts @@ -87,7 +87,9 @@ const checkRegistry = async (): Promise => ).then((policies) => sortPolicies(policies) === sortPolicies(operationPolicies) ? undefined - : error('Front-end has outdated list of operation policies') + : error( + 'Front-end list of operation policies is out of date. To resolve this error, please update "operationPolicies" in specifyweb/frontend/js_src/lib/components/Permissions/definitions.ts based on the response from the http://localhost/permissions/registry/ endpoint' + ) ); export type PermissionsQueryItem = { From 939938fa1159b94dd4d1e23888c29a405c25acab Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Fri, 26 Jul 2024 06:21:26 -0700 Subject: [PATCH 25/27] Only check for isadmin --- .../frontend/js_src/lib/components/SchemaConfig/Tables.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx index 02551310c4f..276956a0286 100644 --- a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx +++ b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx @@ -86,10 +86,8 @@ export function tablesFilter( selectedTables: RA | undefined = undefined ): boolean { if (selectedTables?.includes(name) === true) return true; - const userIsAdmin = - userInformation.isadmin && userInformation.usertype === 'Manager'; - const isRestricted = userIsAdmin + const isRestricted = userInformation.isadmin ? overrides.isHidden : overrides.isHidden || overrides.isSystem; if (!showHiddenTables && isRestricted) return false; From d655704834f106a667d75b0474220a2b2829b5d2 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Fri, 26 Jul 2024 06:31:27 -0700 Subject: [PATCH 26/27] Chnage test --- .../js_src/lib/components/SchemaConfig/__tests__/Tables.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/SchemaConfig/__tests__/Tables.test.ts b/specifyweb/frontend/js_src/lib/components/SchemaConfig/__tests__/Tables.test.ts index 53ced8e3977..4e684bb9a72 100644 --- a/specifyweb/frontend/js_src/lib/components/SchemaConfig/__tests__/Tables.test.ts +++ b/specifyweb/frontend/js_src/lib/components/SchemaConfig/__tests__/Tables.test.ts @@ -15,7 +15,7 @@ describe('tablesFilter', () => { )); test('showHiddenTables excludes hidden and system', () => - expect(tablesFilter(false, true, true, tables.Institution)).toBe(false)); + expect(tablesFilter(false, true, true, tables.Institution)).toBe(true)); test('showNoAccessTables excludes table without permission', () => { const tablePermissions = From 436580f0ff3eff3e6e85f72c85d2f4db016cf0aa Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:05:01 -0500 Subject: [PATCH 27/27] Add border to query items in dialogs Fixes #5149 --- .../js_src/lib/components/WbPlanView/CustomSelectElement.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx index ce93e0177cb..e9fb4350e16 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx @@ -574,7 +574,7 @@ export function CustomSelectElement({ aria-haspopup="listbox" className={` flex min-h-[theme(spacing.8)] min-w-max cursor-pointer - items-center gap-1 rounded px-1 text-left + items-center gap-1 rounded border border-gray-500 px-1 text-left md:min-w-[unset] dark:border-none ${ defaultOption?.isRequired === true