Skip to content

Commit 3c85af2

Browse files
authored
Merge pull request #153 from multinet-app/session-model-view
Add "Session" model to capture session-based Trrack information
2 parents 4d50da5 + 36b249b commit 3c85af2

File tree

10 files changed

+280
-5
lines changed

10 files changed

+280
-5
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Generated by Django 3.2.16 on 2023-05-18 15:23
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import django_extensions.db.fields
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('api', '0013_alter_upload_data_type'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='TableSession',
17+
fields=[
18+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19+
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
20+
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
21+
('name', models.CharField(max_length=300)),
22+
('visapp', models.CharField(max_length=64)),
23+
('state', models.JSONField()),
24+
('table', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.table')),
25+
],
26+
options={
27+
'abstract': False,
28+
},
29+
),
30+
migrations.CreateModel(
31+
name='NetworkSession',
32+
fields=[
33+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
34+
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
35+
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
36+
('name', models.CharField(max_length=300)),
37+
('visapp', models.CharField(max_length=64)),
38+
('state', models.JSONField()),
39+
('network', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.network')),
40+
],
41+
options={
42+
'abstract': False,
43+
},
44+
),
45+
]

multinet/api/models/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from .network import Network
2+
from .session import NetworkSession, TableSession
23
from .table import Table, TableTypeAnnotation
34
from .tasks import AqlQuery, Task, Upload
45
from .workspace import Workspace, WorkspaceRole, WorkspaceRoleChoice
56

67
__all__ = [
78
'AqlQuery',
89
'Network',
10+
'NetworkSession',
911
'Table',
12+
'TableSession',
1013
'TableTypeAnnotation',
1114
'Task',
1215
'Upload',

multinet/api/models/session.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from django.db import models
2+
from django_extensions.db.models import TimeStampedModel
3+
4+
from .network import Network
5+
from .table import Table
6+
7+
8+
class Session(TimeStampedModel):
9+
name = models.CharField(max_length=300)
10+
11+
visapp = models.CharField(max_length=64)
12+
state = models.JSONField()
13+
14+
class Meta:
15+
abstract = True
16+
17+
18+
class TableSession(Session):
19+
table = models.ForeignKey(Table, on_delete=models.CASCADE)
20+
21+
22+
class NetworkSession(Session):
23+
network = models.ForeignKey(Network, on_delete=models.CASCADE)

multinet/api/views/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .network import NetworkViewSet
22
from .query import AqlQueryViewSet
3+
from .session import NetworkSessionViewSet, TableSessionViewSet
34
from .table import TableViewSet
45
from .upload import UploadViewSet
56
from .users import users_me_view, users_search_view
@@ -8,7 +9,9 @@
89
__all__ = [
910
'users_me_view',
1011
'users_search_view',
12+
'NetworkSessionViewSet',
1113
'NetworkViewSet',
14+
'TableSessionViewSet',
1215
'TableViewSet',
1316
'UploadViewSet',
1417
'WorkspaceViewSet',

multinet/api/views/common.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,28 @@ def paginate_queryset(self, query: ArangoQuery, request: Request) -> List[Dict]:
4242

4343

4444
class WorkspaceChildMixin(NestedViewSetMixin):
45+
prefix = None
46+
47+
@property
48+
def workspace_field(self):
49+
field = 'workspace__name'
50+
if self.prefix is not None:
51+
field = f'{self.prefix}__{field}'
52+
53+
return field
54+
55+
def get_parents_query_dict(self):
56+
parents_query_dict = super().get_parents_query_dict()
57+
58+
# Replace the standard lookup field with one that (possibly) goes
59+
# through the session object's related network or table object.
60+
new_field = self.workspace_field
61+
if new_field not in parents_query_dict:
62+
old_field = 'workspace__name'
63+
parents_query_dict[new_field] = parents_query_dict.pop(old_field)
64+
65+
return parents_query_dict
66+
4567
def get_queryset(self):
4668
"""
4769
Get the queryset for workspace child enpoints.
@@ -56,7 +78,7 @@ def get_queryset(self):
5678

5779
parent_query_dict = self.get_parents_query_dict()
5880
workspace = get_object_or_404(
59-
Workspace.objects.select_related('owner'), name=parent_query_dict['workspace__name']
81+
Workspace.objects.select_related('owner'), name=parent_query_dict[self.workspace_field]
6082
)
6183

6284
# No user or user permission required for public workspaces
@@ -78,3 +100,11 @@ def get_queryset(self):
78100

79101
# Read access denied
80102
raise Http404
103+
104+
105+
class NetworkWorkspaceChildMixin(WorkspaceChildMixin):
106+
prefix = 'network'
107+
108+
109+
class TableWorkspaceChildMixin(WorkspaceChildMixin):
110+
prefix = 'table'

multinet/api/views/network.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from rest_framework_extensions.mixins import DetailSerializerMixin
1111

1212
from multinet.api.auth.decorators import require_workspace_permission
13-
from multinet.api.models import Network, Table, Workspace, WorkspaceRoleChoice
13+
from multinet.api.models import Network, NetworkSession, Table, Workspace, WorkspaceRoleChoice
1414
from multinet.api.tasks.upload.csv import create_csv_network
1515
from multinet.api.utils.arango import ArangoQuery
1616
from multinet.api.views.serializers import (
@@ -20,6 +20,7 @@
2020
NetworkReturnDetailSerializer,
2121
NetworkReturnSerializer,
2222
NetworkSerializer,
23+
NetworkSessionSerializer,
2324
NetworkTablesSerializer,
2425
PaginatedResultSerializer,
2526
TableReturnSerializer,
@@ -204,3 +205,17 @@ def tables(self, request, parent_lookup_workspace__name: str, name: str):
204205
serializer = TableReturnSerializer(network_tables, many=True)
205206

206207
return Response(serializer.data, status=status.HTTP_200_OK)
208+
209+
@swagger_auto_schema(
210+
responses={200: NetworkSessionSerializer(many=True)},
211+
)
212+
@action(detail=True, url_path='sessions')
213+
@require_workspace_permission(WorkspaceRoleChoice.READER)
214+
def sessions(self, request, parent_lookup_workspace__name: str, name: str):
215+
workspace: Workspace = get_object_or_404(Workspace, name=parent_lookup_workspace__name)
216+
network: Network = get_object_or_404(Network, workspace=workspace, name=name)
217+
218+
sessions = NetworkSession.objects.filter(network=network.id)
219+
serializer = NetworkSessionSerializer(sessions, many=True)
220+
221+
return Response(serializer.data, status=status.HTTP_200_OK)

multinet/api/views/serializers.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22
from django.contrib.auth.validators import UnicodeUsernameValidator
33
from rest_framework import serializers
44

5-
from multinet.api.models import AqlQuery, Network, Table, TableTypeAnnotation, Upload, Workspace
5+
from multinet.api.models import (
6+
AqlQuery,
7+
Network,
8+
NetworkSession,
9+
Table,
10+
TableSession,
11+
TableTypeAnnotation,
12+
Upload,
13+
Workspace,
14+
)
615

716

817
# The default ModelSerializer for User fails if the user already exists
@@ -230,6 +239,18 @@ class NetworkTablesSerializer(serializers.Serializer):
230239
type = serializers.ChoiceField(choices=['node', 'edge', 'all'], default='all', required=False)
231240

232241

242+
class NetworkSessionSerializer(serializers.ModelSerializer):
243+
class Meta:
244+
model = NetworkSession
245+
fields = '__all__'
246+
247+
248+
class TableSessionSerializer(serializers.ModelSerializer):
249+
class Meta:
250+
model = TableSession
251+
fields = '__all__'
252+
253+
233254
class UploadCreateSerializer(serializers.Serializer):
234255
field_value = serializers.CharField()
235256

multinet/api/views/session.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from django.http.response import Http404
2+
from django.shortcuts import get_object_or_404
3+
from drf_yasg.utils import swagger_auto_schema
4+
from rest_framework import serializers, status
5+
from rest_framework.decorators import action
6+
from rest_framework.mixins import (
7+
CreateModelMixin,
8+
DestroyModelMixin,
9+
ListModelMixin,
10+
RetrieveModelMixin,
11+
)
12+
from rest_framework.response import Response
13+
from rest_framework.viewsets import GenericViewSet
14+
15+
from ..auth.decorators import require_workspace_permission
16+
from ..models import NetworkSession, TableSession, Workspace, WorkspaceRoleChoice
17+
from .common import NetworkWorkspaceChildMixin, TableWorkspaceChildMixin
18+
from .serializers import NetworkSessionSerializer, TableSessionSerializer
19+
20+
21+
class SessionCreateSerializer(serializers.Serializer):
22+
workspace = serializers.CharField()
23+
network = serializers.CharField(required=False)
24+
table = serializers.CharField(required=False)
25+
26+
visapp = serializers.CharField()
27+
name = serializers.CharField()
28+
29+
def validate(self, data):
30+
if not bool(data.get('network')) ^ bool(data.get('table')):
31+
raise serializers.ValidationError('exactly one of `network` or `table` is required')
32+
33+
return data
34+
35+
36+
class SessionStatePatchSerializer(serializers.Serializer):
37+
state = serializers.JSONField()
38+
39+
40+
class SessionNamePatchSerializer(serializers.Serializer):
41+
name = serializers.CharField(max_length=300)
42+
43+
44+
class SessionViewSet(
45+
CreateModelMixin, RetrieveModelMixin, DestroyModelMixin, ListModelMixin, GenericViewSet
46+
):
47+
swagger_tags = ['sessions']
48+
49+
@swagger_auto_schema(request_body=SessionStatePatchSerializer)
50+
@action(detail=True, methods=['patch'])
51+
@require_workspace_permission(WorkspaceRoleChoice.WRITER)
52+
def state(self, request, parent_lookup_workspace__name: str, pk=None):
53+
session = self.get_object()
54+
55+
workspace: Workspace = get_object_or_404(Workspace, name=parent_lookup_workspace__name)
56+
session_ws = (
57+
session.table.workspace if hasattr(session, 'table') else session.network.workspace
58+
)
59+
if workspace.id != session_ws.id:
60+
raise Http404
61+
62+
serializer = SessionStatePatchSerializer(data=request.data)
63+
serializer.is_valid(raise_exception=True)
64+
data = serializer.validated_data['state']
65+
66+
session.state = data
67+
session.save()
68+
69+
return Response(status=status.HTTP_204_NO_CONTENT)
70+
71+
@swagger_auto_schema(request_body=SessionNamePatchSerializer)
72+
@action(detail=True, methods=['patch'], url_path='name')
73+
@require_workspace_permission(WorkspaceRoleChoice.WRITER)
74+
def set_name(self, request, parent_lookup_workspace__name: str, pk=None):
75+
session = self.get_object()
76+
77+
workspace: Workspace = get_object_or_404(Workspace, name=parent_lookup_workspace__name)
78+
session_ws = (
79+
session.table.workspace if hasattr(session, 'table') else session.network.workspace
80+
)
81+
if workspace.id != session_ws.id:
82+
raise Http404
83+
84+
serializer = SessionNamePatchSerializer(data=request.data)
85+
serializer.is_valid(raise_exception=True)
86+
name = serializer.validated_data['name']
87+
88+
session.name = name
89+
session.save()
90+
91+
return Response(status=status.HTTP_204_NO_CONTENT)
92+
93+
94+
class NetworkSessionViewSet(NetworkWorkspaceChildMixin, SessionViewSet):
95+
queryset = NetworkSession.objects.all().select_related('network__workspace')
96+
serializer_class = NetworkSessionSerializer
97+
98+
99+
class TableSessionViewSet(TableWorkspaceChildMixin, SessionViewSet):
100+
queryset = TableSession.objects.all().select_related('table__workspace')
101+
serializer_class = TableSessionSerializer

multinet/api/views/table.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,21 @@
1212
from rest_framework.viewsets import ReadOnlyModelViewSet
1313

1414
from multinet.api.auth.decorators import require_workspace_permission
15-
from multinet.api.models import Table, TableTypeAnnotation, Workspace, WorkspaceRoleChoice
15+
from multinet.api.models import (
16+
Table,
17+
TableSession,
18+
TableTypeAnnotation,
19+
Workspace,
20+
WorkspaceRoleChoice,
21+
)
1622
from multinet.api.utils.arango import ArangoQuery
1723
from multinet.api.views.serializers import (
1824
PaginatedResultSerializer,
1925
TableCreateSerializer,
2026
TableReturnSerializer,
2127
TableRowRetrieveSerializer,
2228
TableSerializer,
29+
TableSessionSerializer,
2330
)
2431

2532
from .common import ArangoPagination, MultinetPagination, WorkspaceChildMixin
@@ -152,3 +159,17 @@ def get_type_annotations(self, request, parent_lookup_workspace__name: str, name
152159
annotations = TableTypeAnnotation.objects.all().filter(table=table)
153160
annotations_dict = {ann.column: ann.type for ann in annotations}
154161
return Response(annotations_dict, status=status.HTTP_200_OK)
162+
163+
@swagger_auto_schema(
164+
responses={200: TableSessionSerializer(many=True)},
165+
)
166+
@action(detail=True, url_path='sessions')
167+
@require_workspace_permission(WorkspaceRoleChoice.READER)
168+
def sessions(self, request, parent_lookup_workspace__name: str, name: str):
169+
workspace: Workspace = get_object_or_404(Workspace, name=parent_lookup_workspace__name)
170+
table: Table = get_object_or_404(Table, workspace=workspace, name=name)
171+
172+
sessions = TableSession.objects.filter(table=table.id)
173+
serializer = TableSessionSerializer(sessions, many=True)
174+
175+
return Response(serializer.data, status=status.HTTP_200_OK)

multinet/urls.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
from multinet.api.views import (
1010
AqlQueryViewSet,
11+
NetworkSessionViewSet,
1112
NetworkViewSet,
13+
TableSessionViewSet,
1214
TableViewSet,
1315
UploadViewSet,
1416
WorkspaceViewSet,
@@ -42,7 +44,18 @@
4244
basename='query',
4345
parents_query_lookups=[f'workspace__{WorkspaceViewSet.lookup_field}'],
4446
)
45-
47+
workspaces_routes.register(
48+
'sessions/network',
49+
NetworkSessionViewSet,
50+
basename='session',
51+
parents_query_lookups=[f'workspace__{WorkspaceViewSet.lookup_field}'],
52+
)
53+
workspaces_routes.register(
54+
'sessions/table',
55+
TableSessionViewSet,
56+
basename='session',
57+
parents_query_lookups=[f'workspace__{WorkspaceViewSet.lookup_field}'],
58+
)
4659

4760
# OpenAPI generation
4861
schema_view = get_schema_view(

0 commit comments

Comments
 (0)