Commit 98621b4643d32eb06541e22b72f79fb0d8f5e8c8
1 parent
42cd9e29
Exists in
dev
add DRF, pyros api views, pyros_api python script, fixing period assignation whe…
…n submitting a sequence
Showing
17 changed files
with
236 additions
and
6 deletions
Show diff stats
CHANGELOG
1 | +08-02-2022 (AKo) : v0.3.6.0 | ||
2 | + - Add DRF (Django Rest Framework) to requirements | ||
3 | + - Add Pyros API (User and sequences with sequence submission) | ||
4 | + - Add PyrosAPI script to use the API via Python command line | ||
5 | + - Fixed error when submitting the sequence : the period wasn't associated | ||
6 | + | ||
1 | 07-02-2022 (AKo) : v0.3.5.0 | 7 | 07-02-2022 (AKo) : v0.3.5.0 |
2 | - Adding INVENTORY section to observatory configuration | 8 | - Adding INVENTORY section to observatory configuration |
3 | - Logout now redirect to home page | 9 | - Logout now redirect to home page |
VERSION
install/requirements.txt
@@ -5,7 +5,8 @@ Django==3.2 | @@ -5,7 +5,8 @@ Django==3.2 | ||
5 | django-admin-tools==0.9.1 | 5 | django-admin-tools==0.9.1 |
6 | django-bootstrap3==21.1 | 6 | django-bootstrap3==21.1 |
7 | django-extensions==3.1.5 | 7 | django-extensions==3.1.5 |
8 | - | 8 | +# for Django Rest Framework (DRF) |
9 | +djangorestframework | ||
9 | # for Choices | 10 | # for Choices |
10 | django-model-utils==4.2.0 | 11 | django-model-utils==4.2.0 |
11 | django-suit==0.2.28 | 12 | django-suit==0.2.28 |
@@ -0,0 +1,86 @@ | @@ -0,0 +1,86 @@ | ||
1 | +import requests, sys, yaml | ||
2 | + | ||
3 | +class PyrosAPI: | ||
4 | + """ | ||
5 | + Request Pyros API with an PyrosUser account linked to the username and password | ||
6 | + """ | ||
7 | + BASE_PYROS_URL = "http://localhost:8000/api/" | ||
8 | + def __init__(self, username : str, password : str ) -> None: | ||
9 | + """ | ||
10 | + Initialize PyrosAPI class by getting the authentification token required to use the API | ||
11 | + | ||
12 | + Args: | ||
13 | + username : Username of the user (mail adress) | ||
14 | + password : Password of the user | ||
15 | + """ | ||
16 | + self.get_token(username, password) | ||
17 | + | ||
18 | + | ||
19 | + def get_token(self,username : str,password : str): | ||
20 | + """ | ||
21 | + get the authentification token linked to the user account | ||
22 | + | ||
23 | + Args: | ||
24 | + username : Username of the user (mail adress) | ||
25 | + password : Password of the user | ||
26 | + """ | ||
27 | + url = f"{self.BASE_PYROS_URL}api-token-auth/" | ||
28 | + request = requests.post(url, data={"username":username,"password":password}) | ||
29 | + json_response = request.json() | ||
30 | + self.token = json_response["token"] | ||
31 | + print(f"Token is {self.token}") | ||
32 | + | ||
33 | + def get_url(self,url : str) -> dict: | ||
34 | + """ | ||
35 | + Query the url and return the response as json | ||
36 | + | ||
37 | + Args: | ||
38 | + url : Url to be requested without the base url of the website | ||
39 | + | ||
40 | + Returns: | ||
41 | + dict : Json object that represents the response of the API | ||
42 | + """ | ||
43 | + headers = {"Authorization" : f"Token {self.token}"} | ||
44 | + | ||
45 | + request = requests.get(self.BASE_PYROS_URL+url, headers=headers) | ||
46 | + return request.json() | ||
47 | + | ||
48 | + def submit_sequence_file(self,file : str) -> dict: | ||
49 | + """ | ||
50 | + Submit sequence file by querying the API via PUT method | ||
51 | + | ||
52 | + Args: | ||
53 | + file : File path to the sequence file to be uploaded | ||
54 | + | ||
55 | + Returns: | ||
56 | + dict : Json object that represents the response of the API | ||
57 | + """ | ||
58 | + headers = { | ||
59 | + "Authorization" : f"Token {self.token}", | ||
60 | + 'Content-type': 'application/json', | ||
61 | + } | ||
62 | + url = f"{self.BASE_PYROS_URL}submit_sequence" | ||
63 | + yaml_file = open(file,"r") | ||
64 | + sequence_file_as_dict = yaml.safe_load(yaml_file) | ||
65 | + print(f"File content : {sequence_file_as_dict}") | ||
66 | + request = requests.put(url, headers=headers, json=sequence_file_as_dict) | ||
67 | + print(f"Request status code {request.status_code}") | ||
68 | + return request.json() | ||
69 | + | ||
70 | + | ||
71 | +def main(): | ||
72 | + username = sys.argv[1] | ||
73 | + password = sys.argv[2] | ||
74 | + url = sys.argv[3] | ||
75 | + yaml_file = None | ||
76 | + api = PyrosAPI(username,password) | ||
77 | + if len(sys.argv) > 4 and sys.argv[4]: | ||
78 | + yaml_file = sys.argv[4] | ||
79 | + print(api.submit_sequence_file(yaml_file)) | ||
80 | + else: | ||
81 | + print(username,password,url) | ||
82 | + print(api.get_url(url)) | ||
83 | + | ||
84 | + | ||
85 | +if __name__ == "__main__": | ||
86 | + main() |
@@ -0,0 +1,16 @@ | @@ -0,0 +1,16 @@ | ||
1 | +from common.models import PyrosUser, Sequence | ||
2 | +from rest_framework import serializers | ||
3 | + | ||
4 | +class UserSerializer(serializers.ModelSerializer): | ||
5 | + class Meta: | ||
6 | + model = PyrosUser | ||
7 | + fields = ['url', 'username', 'email', 'institute', "user_level", "motive_of_registration"] | ||
8 | + depth = 1 | ||
9 | + | ||
10 | + | ||
11 | + | ||
12 | +class SequenceSerializer(serializers.ModelSerializer): | ||
13 | + pyros_user = UserSerializer(read_only=True) | ||
14 | + class Meta: | ||
15 | + model = Sequence | ||
16 | + fields = "__all__" | ||
0 | \ No newline at end of file | 17 | \ No newline at end of file |
@@ -0,0 +1,7 @@ | @@ -0,0 +1,7 @@ | ||
1 | +from src.core.pyros_django import pyros_settings | ||
2 | + | ||
3 | + | ||
4 | +REST_FRAMEWORK = { | ||
5 | + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', | ||
6 | + 'PAGE_SIZE': pyros_settings.NB_ELEMENT_PER_PAGE | ||
7 | +} | ||
0 | \ No newline at end of file | 8 | \ No newline at end of file |
@@ -0,0 +1,14 @@ | @@ -0,0 +1,14 @@ | ||
1 | +from django.urls import path, include | ||
2 | +from . import views | ||
3 | +from rest_framework.authtoken.views import obtain_auth_token | ||
4 | +from rest_framework import routers | ||
5 | + | ||
6 | +router = routers.DefaultRouter() | ||
7 | +router.register(r'users', views.UserViewSet) | ||
8 | +router.register(r'sequences', views.SequenceViewSet) | ||
9 | +urlpatterns = [ | ||
10 | + path('', include(router.urls)), | ||
11 | + path('hello/', views.Users.as_view(), name='hello'), | ||
12 | + path('api-token-auth/', obtain_auth_token, name='api_token_auth'), | ||
13 | + path("submit_sequence",views.submit_sequence_with_json, name="api_submit_sequence"), | ||
14 | +] + router.urls | ||
0 | \ No newline at end of file | 15 | \ No newline at end of file |
@@ -0,0 +1,63 @@ | @@ -0,0 +1,63 @@ | ||
1 | +from django.shortcuts import render | ||
2 | +from rest_framework.views import APIView | ||
3 | +from rest_framework.response import Response | ||
4 | +from rest_framework import viewsets | ||
5 | +from rest_framework.permissions import IsAuthenticated, AllowAny | ||
6 | +from rest_framework.decorators import api_view, permission_classes | ||
7 | +from django.core.validators import ValidationError | ||
8 | +from src.core.pyros_django.user_manager import views as user_views | ||
9 | +from api.serializers import SequenceSerializer, UserSerializer | ||
10 | +from common.models import PyrosUser, Sequence | ||
11 | +from routine_manager.functions import check_sequence_file_validity | ||
12 | +# Create your views here. | ||
13 | + | ||
14 | + | ||
15 | +class Users(APIView): | ||
16 | + | ||
17 | + permission_classes = (IsAuthenticated,) | ||
18 | + | ||
19 | + def get(self, request): | ||
20 | + content = {"message" : "Hello"} | ||
21 | + return Response(content) | ||
22 | + | ||
23 | + @api_view(["GET"]) | ||
24 | + def user_logout(request): | ||
25 | + | ||
26 | + request.user.auth_token.delete() | ||
27 | + | ||
28 | + user_views.logout() | ||
29 | + | ||
30 | + return Response('User Logged out successfully') | ||
31 | + | ||
32 | + | ||
33 | +class UserViewSet(viewsets.ModelViewSet): | ||
34 | + """ | ||
35 | + API endpoint that allows users to be viewed or edited. | ||
36 | + """ | ||
37 | + queryset = PyrosUser.objects.all().order_by('-date_joined') | ||
38 | + serializer_class = UserSerializer | ||
39 | + permission_classes = [IsAuthenticated] | ||
40 | + | ||
41 | + | ||
42 | +class SequenceViewSet(viewsets.ModelViewSet): | ||
43 | + """ | ||
44 | + API endpoint that allows users to be viewed or edited. | ||
45 | + """ | ||
46 | + queryset = Sequence.objects.all().order_by("-created") | ||
47 | + serializer_class = SequenceSerializer | ||
48 | + permission_classes = [IsAuthenticated] | ||
49 | + | ||
50 | + def get_queryset(self): | ||
51 | + """ | ||
52 | + This view should return a list of all the purchases | ||
53 | + for the currently authenticated user. | ||
54 | + """ | ||
55 | + user = self.request.user | ||
56 | + return Sequence.objects.filter(pyros_user=user).order_by("-created") | ||
57 | + | ||
58 | +@api_view(["PUT"]) | ||
59 | +def submit_sequence_with_json(request): | ||
60 | + sequence_json = request.data | ||
61 | + print(sequence_json) | ||
62 | + response = check_sequence_file_validity(sequence_json,request) | ||
63 | + return Response(response) |
src/core/pyros_django/pyros/settings.py
@@ -192,6 +192,10 @@ INSTALLED_APPS = [ | @@ -192,6 +192,10 @@ INSTALLED_APPS = [ | ||
192 | 'test_without_migrations', | 192 | 'test_without_migrations', |
193 | 'bootstrap3', | 193 | 'bootstrap3', |
194 | 194 | ||
195 | + # Django Rest Framework | ||
196 | + "rest_framework", | ||
197 | + "rest_framework.authtoken", | ||
198 | + | ||
195 | # PYROS APPS | 199 | # PYROS APPS |
196 | 'dashboard', | 200 | 'dashboard', |
197 | 'scheduler', | 201 | 'scheduler', |
@@ -206,9 +210,17 @@ INSTALLED_APPS = [ | @@ -206,9 +210,17 @@ INSTALLED_APPS = [ | ||
206 | 'devices', | 210 | 'devices', |
207 | 'scientific_program', | 211 | 'scientific_program', |
208 | 'obsconfig', | 212 | 'obsconfig', |
213 | + "api", | ||
209 | #'kombu.transport.django' | 214 | #'kombu.transport.django' |
210 | ] | 215 | ] |
211 | 216 | ||
217 | +REST_FRAMEWORK = { | ||
218 | + 'DEFAULT_AUTHENTICATION_CLASSES': [ | ||
219 | + 'rest_framework.authentication.TokenAuthentication', | ||
220 | + 'rest_framework.authentication.SessionAuthentication', | ||
221 | + ], | ||
222 | +} | ||
223 | + | ||
212 | MIDDLEWARE = [ | 224 | MIDDLEWARE = [ |
213 | 'django.middleware.security.SecurityMiddleware', | 225 | 'django.middleware.security.SecurityMiddleware', |
214 | 'django.contrib.sessions.middleware.SessionMiddleware', | 226 | 'django.contrib.sessions.middleware.SessionMiddleware', |
src/core/pyros_django/pyros/urls.py
@@ -42,6 +42,7 @@ urlpatterns = [ | @@ -42,6 +42,7 @@ urlpatterns = [ | ||
42 | url(r'^user_manager/', include('user_manager.urls')), | 42 | url(r'^user_manager/', include('user_manager.urls')), |
43 | url(r'^scientific_program/', include('scientific_program.urls')), | 43 | url(r'^scientific_program/', include('scientific_program.urls')), |
44 | url(r'^obs_config/', include('obsconfig.urls')), | 44 | url(r'^obs_config/', include('obsconfig.urls')), |
45 | + url(r'^api/', include('api.urls')), | ||
45 | url(r'^$', dashboard.views.index) | 46 | url(r'^$', dashboard.views.index) |
46 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) | 47 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) |
47 | 48 |
src/core/pyros_django/routine_manager/functions.py
1 | import ast | 1 | import ast |
2 | from src.core.pyros_django.obsconfig.obsconfig_class import OBSConfig | 2 | from src.core.pyros_django.obsconfig.obsconfig_class import OBSConfig |
3 | from .forms import SequenceForm, AlbumForm, PlanForm | 3 | from .forms import SequenceForm, AlbumForm, PlanForm |
4 | -from common.models import PyrosUser, ScientificProgram, Sequence, Album, Plan | 4 | +from common.models import PyrosUser, ScientificProgram, Sequence, Album, Plan, Period |
5 | import datetime | 5 | import datetime |
6 | import os, yaml | 6 | import os, yaml |
7 | 7 | ||
@@ -19,8 +19,9 @@ def check_sequence_file_validity(yaml_content,request): | @@ -19,8 +19,9 @@ def check_sequence_file_validity(yaml_content,request): | ||
19 | "errors": [], | 19 | "errors": [], |
20 | } | 20 | } |
21 | index_value_sp = yaml_content["sequence"]["scientific_program"]["value"] | 21 | index_value_sp = yaml_content["sequence"]["scientific_program"]["value"] |
22 | + values = yaml_content["sequence"]["scientific_program"]["values"] | ||
22 | if index_value_sp < 0 or index_value_sp > len(yaml_content["sequence"]["scientific_program"]["values"]): | 23 | if index_value_sp < 0 or index_value_sp > len(yaml_content["sequence"]["scientific_program"]["values"]): |
23 | - result["errors"].append(f"Value of scientific program isn't valid, index out of bounds") | 24 | + result["errors"].append(f"Value of scientific program isn't valid, index out of bounds ({index_value_sp} > {len(values)})") |
24 | index_value_sp = 0 | 25 | index_value_sp = 0 |
25 | chosen_sp = ScientificProgram.objects.get(name=yaml_content["sequence"]["scientific_program"]["values"][index_value_sp]) | 26 | chosen_sp = ScientificProgram.objects.get(name=yaml_content["sequence"]["scientific_program"]["values"][index_value_sp]) |
26 | if chosen_sp in sp_list: | 27 | if chosen_sp in sp_list: |
@@ -37,8 +38,9 @@ def check_sequence_file_validity(yaml_content,request): | @@ -37,8 +38,9 @@ def check_sequence_file_validity(yaml_content,request): | ||
37 | else: | 38 | else: |
38 | if yaml_content["sequence"][field].get("values"): | 39 | if yaml_content["sequence"][field].get("values"): |
39 | index_value = yaml_content["sequence"][field]["value"] | 40 | index_value = yaml_content["sequence"][field]["value"] |
41 | + values = yaml_content["sequence"][field]["values"] | ||
40 | if index_value < 0 or index_value > len(yaml_content["sequence"][field]["values"]): | 42 | if index_value < 0 or index_value > len(yaml_content["sequence"][field]["values"]): |
41 | - result["errors"].append(f"Value of {field} isn't valid, index out of bounds") | 43 | + result["errors"].append(f"Value of {field} isn't valid, index out of bounds ({index_value} > {len(values)})") |
42 | index_value = 0 | 44 | index_value = 0 |
43 | value = yaml_content["sequence"][field]["values"][index_value] | 45 | value = yaml_content["sequence"][field]["values"][index_value] |
44 | else: | 46 | else: |
@@ -102,8 +104,9 @@ def check_sequence_file_validity(yaml_content,request): | @@ -102,8 +104,9 @@ def check_sequence_file_validity(yaml_content,request): | ||
102 | else: | 104 | else: |
103 | if plan[field].get("values"): | 105 | if plan[field].get("values"): |
104 | index_value = plan[field]["value"] | 106 | index_value = plan[field]["value"] |
107 | + values = plan[field]["values"] | ||
105 | if index_value < 0 or index_value > len(plan[field]["values"]): | 108 | if index_value < 0 or index_value > len(plan[field]["values"]): |
106 | - result["errors"].append(f"Value of Plan field '{field}' isn't valid, index out of bounds") | 109 | + result["errors"].append(f"Value of Plan field '{field}' isn't valid, index out of bounds ({index_value} > {len(values)})") |
107 | index_value = 0 | 110 | index_value = 0 |
108 | value = plan[field]["values"][index_value] | 111 | value = plan[field]["values"][index_value] |
109 | try: | 112 | try: |
@@ -129,9 +132,15 @@ def check_sequence_file_validity(yaml_content,request): | @@ -129,9 +132,15 @@ def check_sequence_file_validity(yaml_content,request): | ||
129 | 132 | ||
130 | seq.status = Sequence.TOBEPLANNED | 133 | seq.status = Sequence.TOBEPLANNED |
131 | seq.complete = True | 134 | seq.complete = True |
135 | + period = Period.objects.exploitation_period() | ||
136 | + if Period.objects.next_period() != None and Period.objects.next_period().start_date < seq.start_date.date(): | ||
137 | + period = Period.objects.next_period() | ||
138 | + seq.period = period | ||
132 | seq.save() | 139 | seq.save() |
133 | if len(result["errors"]) != 0: | 140 | if len(result["errors"]) != 0: |
134 | result["succeed"] = False | 141 | result["succeed"] = False |
135 | seq.delete() | 142 | seq.delete() |
143 | + else: | ||
144 | + result["sequence_id"] = seq.id | ||
136 | return result | 145 | return result |
137 | 146 |