Skip to content

Commit 42a2584

Browse files
committed
Merge remote-tracking branch 'origin/main' into issue-7334
2 parents a68b399 + 172928f commit 42a2584

Some content is hidden

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

44 files changed

+6824
-6667
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import json
2+
3+
from specifyweb.backend.permissions.permissions import table_permissions_checker
4+
from django.http import (HttpResponse, HttpResponseNotAllowed)
5+
6+
from specifyweb.specify.api import HttpResponseCreated, _obj_to_data, post_resource, toJson
7+
8+
def collection_dispatch_bulk_copy(request, model, copies) -> HttpResponse:
9+
checker = table_permissions_checker(request.specify_collection, request.specify_user_agent, "read")
10+
11+
if request.method != 'POST':
12+
return HttpResponseNotAllowed(['POST'])
13+
14+
data = json.loads(request.body)
15+
data = dict(filter(lambda item: item[0] != 'id', data.items())) # Remove ID field before making copies
16+
resp_objs = []
17+
for _ in range(int(copies)):
18+
obj = post_resource(
19+
request.specify_collection,
20+
request.specify_user_agent,
21+
model,
22+
data,
23+
request.GET.get("recordsetid", None),
24+
)
25+
resp_objs.append(_obj_to_data(obj, checker))
26+
27+
return HttpResponseCreated(toJson(resp_objs), content_type='application/json')
28+
29+
def collection_dispatch_bulk(request, model) -> HttpResponse:
30+
"""
31+
Do the same as collection_dispatch, but for bulk POST operations.
32+
Call this endpoint with a list of objects of the same type to create.
33+
This reduces the amount of API calls needed to create multiple objects, like when creating multiple carry forwards.
34+
"""
35+
checker = table_permissions_checker(request.specify_collection, request.specify_user_agent, "read")
36+
37+
if request.method != 'POST':
38+
return HttpResponseNotAllowed(['POST'])
39+
40+
data = json.loads(request.body)
41+
resp_objs = []
42+
for obj_data in data:
43+
obj = post_resource(
44+
request.specify_collection,
45+
request.specify_user_agent,
46+
model,
47+
obj_data,
48+
request.GET.get("recordsetid", None),
49+
)
50+
resp_objs.append(_obj_to_data(obj, checker))
51+
52+
return HttpResponseCreated(toJson(resp_objs), content_type='application/json')
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
from django.urls import re_path
3+
4+
from specifyweb.backend.bulk_copy import views
5+
6+
urlpatterns = [
7+
re_path(r'^bulk/(?P<model>\w+)/(?P<copies>\d+)/$', views.collection_bulk_copy), # permissions added
8+
re_path(r'^bulk/(?P<model>\w+)/$', views.collection_bulk), # permissions added
9+
]
10+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from specifyweb.backend.bulk_copy import bulk_copy
2+
from specifyweb.specify.views import api_view
3+
4+
5+
collection_bulk_copy = api_view(bulk_copy.collection_dispatch_bulk_copy)
6+
collection_bulk = api_view(bulk_copy.collection_dispatch_bulk)

specifyweb/backend/series/urls.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.urls import include, path, re_path
2+
3+
from specifyweb.backend.series import views
4+
5+
urlpatterns = [
6+
# retrieve auto numbered fields
7+
re_path(r'^series_autonumber_range', views.series_autonumber_range),
8+
]

specifyweb/backend/series/views.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
2+
3+
import json
4+
5+
from django import http
6+
from django.views.decorators.http import require_POST
7+
from specifyweb.specify.api import get_model
8+
9+
from specifyweb.specify.uiformatters import get_uiformatter_by_name
10+
from specifyweb.specify.views import login_maybe_required
11+
12+
@login_maybe_required
13+
@require_POST
14+
def series_autonumber_range(request: http.HttpRequest):
15+
"""
16+
Returns a list of autonumbered values given a range.
17+
Used for series data entry on Collection Objects.
18+
"""
19+
request_data: dict = json.loads(request.body)
20+
range_start = request_data.get('rangestart')
21+
range_end = request_data.get('rangeend')
22+
table_name = request_data.get('tablename')
23+
field_name = request_data.get('fieldname')
24+
formatter_name = request_data.get('formattername')
25+
26+
formatter = get_uiformatter_by_name(request.specify_collection, request.specify_user, formatter_name)
27+
28+
try:
29+
range_start_parsed = formatter.parse(range_start)
30+
assert not formatter.needs_autonumber(range_start_parsed)
31+
canonicalized_range_start = formatter.canonicalize(range_start_parsed)
32+
except:
33+
return http.HttpResponseBadRequest('Range start does not match format.')
34+
try:
35+
range_end_parsed = formatter.parse(range_end)
36+
assert not formatter.needs_autonumber(range_end_parsed)
37+
canonicalized_range_end = formatter.canonicalize(range_end_parsed)
38+
except:
39+
return http.HttpResponseBadRequest('Range end does not match format.')
40+
41+
if canonicalized_range_end <= canonicalized_range_start:
42+
return http.HttpResponseBadRequest(f'Range end must be greater than range start.')
43+
44+
try:
45+
# Repeatedly autonumber until the end is reached.
46+
limit = 500
47+
values = [canonicalized_range_start]
48+
current_value = values[0]
49+
if request_data.get('skipstartnumber'):
50+
# The first value can be optionally excluded/skipped.
51+
# Needed since series entry currently relies on the first record being saved first.
52+
values = []
53+
while current_value < canonicalized_range_end:
54+
current_value = ''.join(formatter.fill_vals_after(current_value))
55+
values.append(current_value)
56+
if len(values) >= limit:
57+
return http.JsonResponse({
58+
'values': [],
59+
'error': 'LimitExceeded',
60+
})
61+
62+
# Check if any existing records use the values.
63+
# Not garanteed to be accurate at the time of saving, just serves as a warning for the frontend.
64+
table = get_model(table_name)
65+
existing_records = table.objects.filter(**{f'{field_name}__in': values, 'collection': request.specify_collection})
66+
existing_values = list(existing_records.values_list(field_name, flat=True))
67+
68+
if len(existing_values) > 0:
69+
return http.JsonResponse({
70+
'values': values,
71+
'existing': existing_values,
72+
'error': 'ExistingNumbers',
73+
})
74+
75+
return http.JsonResponse({
76+
'values': values,
77+
})
78+
except Exception as e:
79+
return http.JsonResponse({'error': 'An internal server error occurred.'}, status=500)

specifyweb/backend/stored_queries/execution.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@
2929
from .field_spec_maps import apply_specify_user_name
3030
from specifyweb.backend.notifications.models import Message
3131
from specifyweb.backend.permissions.permissions import check_table_permissions
32-
from specifyweb.specify.auditlog import auditlog
3332
from specifyweb.specify.models import Loan, Loanpreparation, Loanreturnpreparation, Taxontreedef
3433
from specifyweb.specify.utils import log_sqlalchemy_query
35-
34+
from specifyweb.backend.workbench.upload.auditlog import auditlog
35+
from specifyweb.specify.utils import log_sqlalchemy_query
3636
from specifyweb.backend.stored_queries.group_concat import group_by_displayed_fields
3737
from specifyweb.backend.stored_queries.queryfield import fields_from_json
3838

specifyweb/backend/trees/extras.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from specifyweb.backend.businessrules.exceptions import TreeBusinessRuleException
1818
import specifyweb.specify.models as spmodels
1919

20-
from specifyweb.specify.auditcodes import TREE_BULK_MOVE, TREE_MERGE, TREE_SYNONYMIZE, TREE_DESYNONYMIZE
20+
from specifyweb.backend.workbench.upload.auditcodes import TREE_BULK_MOVE, TREE_MERGE, TREE_SYNONYMIZE, TREE_DESYNONYMIZE
2121

2222
@contextmanager
2323
def validate_node_numbers(table, revalidate_after=True):
@@ -277,7 +277,7 @@ def moving_node(to_save):
277277
to_save.highestchildnodenumber = current.highestchildnodenumber
278278

279279
def mutation_log(action, node, agent, parent, dirty_flds: list[FieldChangeInfo]):
280-
from specifyweb.specify.auditlog import auditlog
280+
from specifyweb.backend.workbench.upload.auditlog import auditlog
281281
auditlog.log_action(action, node, agent, node.parent, dirty_flds)
282282

283283
def merge(node, into, agent):

specifyweb/backend/trees/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from specifyweb.backend.trees.ranks import tree_rank_count
2121
from . import extras
2222
from specifyweb.specify.api import get_object_or_404, obj_to_data, toJson
23-
from specifyweb.specify.auditcodes import TREE_MOVE
23+
from specifyweb.backend.workbench.upload.auditcodes import TREE_MOVE
2424
from specifyweb.backend.trees.stats import get_tree_stats
2525
from specifyweb.specify.views import login_maybe_required, openapi
2626

File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)