Skip to content

Commit af5e271

Browse files
Merge pull request #509 from hinashi/feature/entry_copy_apiv2
Added entry copy apiv2 in new-ui
2 parents b954014 + f2730d6 commit af5e271

File tree

12 files changed

+402
-34
lines changed

12 files changed

+402
-34
lines changed

entity/tests/test_api_v2.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -2289,7 +2289,9 @@ def side_effect(handler_name, entity_name, user, *args):
22892289
self.assertEqual(user, self.user)
22902290

22912291
if handler_name == "before_create_entry":
2292-
self.assertEqual(args[0], {**params, "schema": self.entity})
2292+
self.assertEqual(
2293+
args[0], {**params, "schema": self.entity, "created_user": self.user}
2294+
)
22932295

22942296
if handler_name == "after_create_entry":
22952297
entry = Entry.objects.get(name="hoge", is_active=True)

entry/api_v2/serializers.py

+26-10
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def _validate(self, name: str, schema: Entity, attrs: List[Dict[str, Any]]):
7575

7676
# In create case, check attrs mandatory attribute
7777
if not self.instance:
78-
user: User = User.objects.get(id=self.context["request"].user.id)
78+
user: User = self.context["request"].user
7979
for mandatory_attr in schema.attrs.filter(is_mandatory=True, is_active=True):
8080
if not user.has_permission(mandatory_attr, ACLType.Writable):
8181
raise ValidationError(
@@ -117,29 +117,25 @@ class EntryCreateSerializer(EntryBaseSerializer):
117117
queryset=Entity.objects.all(), write_only=True, required=True
118118
)
119119
attrs = serializers.ListField(child=AttributeSerializer(), write_only=True, required=False)
120-
# created_user = serializers.HiddenField(
121-
# default=serializers.CurrentUserDefault()
122-
# )
120+
created_user = serializers.HiddenField(default=serializers.CurrentUserDefault())
123121

124122
class Meta:
125123
model = Entry
126-
fields = ["id", "name", "schema", "attrs"]
124+
fields = ["id", "name", "schema", "attrs", "created_user"]
127125

128126
def validate(self, params):
129127
self._validate(params["name"], params["schema"], params.get("attrs", []))
130128
return params
131129

132130
def create(self, validated_data):
133-
user: User = User.objects.get(id=self.context["request"].user.id)
131+
user: User = self.context["request"].user
134132

135133
entity_name = validated_data["schema"].name
136134
if custom_view.is_custom("before_create_entry", entity_name):
137135
custom_view.call_custom("before_create_entry", entity_name, user, validated_data)
138136

139137
attrs_data = validated_data.pop("attrs", [])
140-
entry: Entry = Entry.objects.create(
141-
**validated_data, status=Entry.STATUS_CREATING, created_user=user
142-
)
138+
entry: Entry = Entry.objects.create(**validated_data, status=Entry.STATUS_CREATING)
143139

144140
for entity_attr in entry.schema.attrs.filter(is_active=True):
145141
attr: Attribute = entry.add_attribute_from_base(entity_attr, user)
@@ -186,7 +182,7 @@ def validate(self, params):
186182

187183
def update(self, entry: Entry, validated_data):
188184
entry.set_status(Entry.STATUS_EDITING)
189-
user: User = User.objects.get(id=self.context["request"].user.id)
185+
user: User = self.context["request"].user
190186

191187
entity_name = entry.schema.name
192188
if custom_view.is_custom("before_update_entry", entity_name):
@@ -432,6 +428,26 @@ def get_default_attr_value(type: int) -> EntryAttributeValue:
432428
return attrinfo
433429

434430

431+
class EntryCopySerializer(serializers.Serializer):
432+
copy_entry_names = serializers.ListField(
433+
child=serializers.CharField(),
434+
write_only=True,
435+
required=True,
436+
allow_empty=False,
437+
)
438+
439+
class Meta:
440+
fields = "copy_entry_names"
441+
442+
def validate_copy_entry_names(self, copy_entry_names):
443+
entry: Entry = self.instance
444+
for copy_entry_name in copy_entry_names:
445+
if Entry.objects.filter(
446+
name=copy_entry_name, schema=entry.schema, is_active=True
447+
).exists():
448+
raise ValidationError("specified name(%s) already exists" % copy_entry_name)
449+
450+
435451
class GetEntrySimpleSerializer(serializers.ModelSerializer):
436452
class Meta:
437453
model = Entry

entry/api_v2/urls.py

+8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
}
3030
),
3131
),
32+
path(
33+
"<int:pk>/copy/",
34+
views.EntryAPI.as_view(
35+
{
36+
"post": "copy",
37+
}
38+
),
39+
),
3240
path(
3341
"search/",
3442
views.searchAPI.as_view(

entry/api_v2/views.py

+25
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from entry.api_v2.serializers import EntryBaseSerializer
1616
from entry.api_v2.serializers import EntryRetrieveSerializer
1717
from entry.api_v2.serializers import EntryUpdateSerializer
18+
from entry.api_v2.serializers import EntryCopySerializer
1819
from entry.models import AttributeValue, Entry
1920
from job.models import Job
2021
from user.models import User
@@ -28,6 +29,7 @@ def has_object_permission(self, request, view, obj):
2829
"update": ACLType.Writable,
2930
"destroy": ACLType.Full,
3031
"restore": ACLType.Full,
32+
"copy": ACLType.Full,
3133
}
3234

3335
if not user.has_permission(obj, permisson.get(view.action)):
@@ -44,6 +46,7 @@ def get_serializer_class(self):
4446
serializer = {
4547
"retrieve": EntryRetrieveSerializer,
4648
"update": EntryUpdateSerializer,
49+
"copy": EntryCopySerializer,
4750
}
4851
return serializer.get(self.action, EntryBaseSerializer)
4952

@@ -106,6 +109,28 @@ def restore(self, request, pk):
106109

107110
return Response(status=status.HTTP_201_CREATED)
108111

112+
def copy(self, request, pk):
113+
src_entry: Entry = self.get_object()
114+
115+
if not src_entry.is_active:
116+
raise ValidationError("specified entry is not active")
117+
118+
# validate post parameter
119+
serializer = self.get_serializer(src_entry, data=request.data)
120+
serializer.is_valid(raise_exception=True)
121+
122+
# TODO Conversion to support the old UI
123+
params = {
124+
"new_name_list": request.data["copy_entry_names"],
125+
"post_data": request.data,
126+
}
127+
128+
# run copy job
129+
job = Job.new_copy(request.user, src_entry, text="Preparing to copy entry", params=params)
130+
job.run()
131+
132+
return Response({}, status=status.HTTP_200_OK)
133+
109134

110135
class searchAPI(viewsets.ReadOnlyModelViewSet):
111136
serializer_class = GetEntrySimpleSerializer

entry/tests/test_api_v2.py

+153
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,159 @@ def test_restore_entry_notify(self, mock_task):
978978

979979
self.assertTrue(mock_task.called)
980980

981+
@mock.patch("entry.tasks.copy_entry.delay", mock.Mock(side_effect=tasks.copy_entry))
982+
def test_copy_entry(self):
983+
entry: Entry = self.add_entry(self.user, "entry", self.entity)
984+
params = {"copy_entry_names": ["copy1", "copy2"]}
985+
986+
resp = self.client.post(
987+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
988+
)
989+
self.assertEqual(resp.status_code, 200)
990+
self.assertTrue(
991+
Entry.objects.filter(name="copy1", schema=self.entity, is_active=True).exists()
992+
)
993+
self.assertTrue(
994+
Entry.objects.filter(name="copy2", schema=self.entity, is_active=True).exists()
995+
)
996+
997+
def test_copy_entry_without_permission(self):
998+
entry: Entry = self.add_entry(self.user, "entry", self.entity)
999+
params = {"copy_entry_names": ["copy1"]}
1000+
1001+
# permission nothing entity
1002+
self.entity.is_public = False
1003+
self.entity.save()
1004+
resp = self.client.post(
1005+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
1006+
)
1007+
self.assertEqual(resp.status_code, 403)
1008+
self.assertEqual(
1009+
resp.json(), {"detail": "You do not have permission to perform this action."}
1010+
)
1011+
1012+
# permission readable entity
1013+
self.role.users.add(self.user)
1014+
self.role.permissions.add(self.entity.readable)
1015+
resp = self.client.post(
1016+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
1017+
)
1018+
self.assertEqual(resp.status_code, 403)
1019+
self.assertEqual(
1020+
resp.json(), {"detail": "You do not have permission to perform this action."}
1021+
)
1022+
1023+
# permission writable entity
1024+
self.role.permissions.add(self.entity.writable)
1025+
resp = self.client.post(
1026+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
1027+
)
1028+
self.assertEqual(
1029+
resp.json(), {"detail": "You do not have permission to perform this action."}
1030+
)
1031+
1032+
# permission full entity
1033+
self.role.permissions.add(self.entity.full)
1034+
resp = self.client.post(
1035+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
1036+
)
1037+
self.assertEqual(resp.status_code, 200)
1038+
1039+
params = {"copy_entry_names": ["copy2"]}
1040+
1041+
# permission nothing entry
1042+
entry.is_public = False
1043+
entry.save()
1044+
resp = self.client.post(
1045+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
1046+
)
1047+
self.assertEqual(resp.status_code, 403)
1048+
self.assertEqual(
1049+
resp.json(), {"detail": "You do not have permission to perform this action."}
1050+
)
1051+
1052+
# permission readable entry
1053+
self.role.permissions.add(entry.readable)
1054+
resp = self.client.post(
1055+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
1056+
)
1057+
self.assertEqual(resp.status_code, 403)
1058+
self.assertEqual(
1059+
resp.json(), {"detail": "You do not have permission to perform this action."}
1060+
)
1061+
1062+
# permission writable entry
1063+
self.role.permissions.add(entry.writable)
1064+
resp = self.client.post(
1065+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
1066+
)
1067+
self.assertEqual(resp.status_code, 403)
1068+
self.assertEqual(
1069+
resp.json(), {"detail": "You do not have permission to perform this action."}
1070+
)
1071+
1072+
# permission full entry
1073+
self.role.permissions.add(entry.full)
1074+
resp = self.client.post(
1075+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
1076+
)
1077+
self.assertEqual(resp.status_code, 200)
1078+
1079+
def test_copy_entry_with_invalid_param(self):
1080+
params = {"copy_entry_names": ["copy1"]}
1081+
1082+
resp = self.client.post(
1083+
"/entry/api/v2/%s/copy/" % "hoge", json.dumps(params), "application/json"
1084+
)
1085+
self.assertEqual(resp.status_code, 404)
1086+
1087+
resp = self.client.post(
1088+
"/entry/api/v2/%s/copy/" % 9999, json.dumps(params), "application/json"
1089+
)
1090+
self.assertEqual(resp.status_code, 404)
1091+
self.assertEqual(resp.json(), {"detail": "Not found."})
1092+
1093+
entry = self.add_entry(self.user, "entry", self.entity)
1094+
1095+
params = {}
1096+
resp = self.client.post(
1097+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
1098+
)
1099+
self.assertEqual(resp.status_code, 400)
1100+
self.assertEqual(resp.json(), {"copy_entry_names": ["This field is required."]})
1101+
1102+
params = {"copy_entry_names": "hoge"}
1103+
resp = self.client.post(
1104+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
1105+
)
1106+
self.assertEqual(resp.status_code, 400)
1107+
self.assertEqual(
1108+
resp.json(), {"copy_entry_names": ['Expected a list of items but got type "str".']}
1109+
)
1110+
1111+
params = {"copy_entry_names": [{}]}
1112+
resp = self.client.post(
1113+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
1114+
)
1115+
self.assertEqual(resp.status_code, 400)
1116+
self.assertEqual(resp.json(), {"copy_entry_names": {"0": ["Not a valid string."]}})
1117+
1118+
params = {"copy_entry_names": []}
1119+
resp = self.client.post(
1120+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
1121+
)
1122+
self.assertEqual(resp.status_code, 400)
1123+
self.assertEqual(resp.json(), {"copy_entry_names": ["This list may not be empty."]})
1124+
1125+
params = {"copy_entry_names": ["entry"]}
1126+
resp = self.client.post(
1127+
"/entry/api/v2/%s/copy/" % entry.id, json.dumps(params), "application/json"
1128+
)
1129+
self.assertEqual(resp.status_code, 400)
1130+
self.assertEqual(
1131+
resp.json(), {"copy_entry_names": ["specified name(entry) already exists"]}
1132+
)
1133+
9811134
def test_serach_entry(self):
9821135
ref_entry4 = self.add_entry(self.user, "hoge4", self.ref_entity)
9831136
ref_entry5 = self.add_entry(self.user, "hoge5", self.ref_entity)

frontend/src/apiclient/AironeApiClientV2.ts

+25-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import Cookies from "js-cookie";
22

3-
import {
4-
EntityList as ConstEntityList,
5-
EntryReferralList,
6-
} from "../utils/Constants";
7-
83
import {
94
ACL,
105
AclApi,
@@ -16,6 +11,7 @@ import {
1611
EntryCreate,
1712
EntryRetrieve,
1813
EntryUpdate,
14+
EntryCopy,
1915
EntryBase,
2016
Group,
2117
GroupApi,
@@ -32,6 +28,10 @@ import {
3228
EntityAttrUpdate,
3329
PaginatedGetEntrySimpleList,
3430
} from "apiclient/autogenerated";
31+
import {
32+
EntityList as ConstEntityList,
33+
EntryReferralList,
34+
} from "utils/Constants";
3535

3636
// Get CSRF Token from Cookie set by Django
3737
// see https://docs.djangoproject.com/en/3.2/ref/csrf/
@@ -251,6 +251,26 @@ class AironeApiClientV2 {
251251
);
252252
}
253253

254+
async copyEntry(
255+
id: number,
256+
copyEntryNames: Array<string>
257+
): Promise<EntryCopy> {
258+
return await this.entry.entryApiV2CopyCreate(
259+
{
260+
id,
261+
entryCopy: {
262+
copyEntryNames: copyEntryNames,
263+
},
264+
},
265+
{
266+
headers: {
267+
"Content-Type": "application/json;charset=utf-8",
268+
"X-CSRFToken": getCsrfToken(),
269+
},
270+
}
271+
);
272+
}
273+
254274
async getGroups(): Promise<Group[]> {
255275
return await this.group.groupApiV2GroupsList();
256276
}

frontend/src/apiclient/autogenerated/.openapi-generator/FILES

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
.openapi-generator-ignore
21
apis/AclApi.ts
32
apis/EntityApi.ts
43
apis/EntryApi.ts
@@ -16,6 +15,7 @@ models/EntityDetail.ts
1615
models/EntityList.ts
1716
models/EntityUpdate.ts
1817
models/EntryBase.ts
18+
models/EntryCopy.ts
1919
models/EntryCreate.ts
2020
models/EntryRetrieve.ts
2121
models/EntryRetrieveAttrs.ts
@@ -27,6 +27,7 @@ models/GetEntrySimple.ts
2727
models/Group.ts
2828
models/PaginatedEntityListList.ts
2929
models/PaginatedEntryBaseList.ts
30+
models/PaginatedGetEntrySimpleList.ts
3031
models/UserList.ts
3132
models/UserRetrieve.ts
3233
models/UserRetrieveToken.ts

0 commit comments

Comments
 (0)