Skip to content

Commit 1da43e4

Browse files
committed
Merge branch 'encetamasb-orcaslicer-demo'
2 parents a5513d6 + f6a71dc commit 1da43e4

File tree

7 files changed

+150
-0
lines changed

7 files changed

+150
-0
lines changed

backend/api/urls.py

+6
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@
3939

4040
router.register(r'octo/g_code_files', octoprint_views.GCodeFileView, 'AgentGCodeFile')
4141

42+
4243
urlpatterns = [
44+
path('version', viewsets.SlicerApiVersionView.as_view()),
45+
path('printers', viewsets.SlicerApiPrinterListView.as_view()),
46+
path('files/local', viewsets.SlicerApiUploadView.as_view()),
47+
path('v1/apikey/', viewsets.GetApiKeyView.as_view()),
48+
4349
path('v1/onetimeverificationcodes/verify/', # For compatibility with plugin <= 1.7.0
4450
octoprint_views.OneTimeVerificationCodeVerifyView.as_view(),
4551
),

backend/api/viewsets.py

+100
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33
import os
44
import time
55
import logging
6+
import re
67
from binascii import hexlify
78
from rest_framework import viewsets, mixins
9+
from rest_framework.views import APIView
810
from rest_framework.permissions import IsAuthenticated
911
from rest_framework.decorators import action
1012
from rest_framework.response import Response
1113
from rest_framework.exceptions import ValidationError
1214
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
15+
from rest_framework import authentication
1316
from rest_framework import status
17+
from rest_framework.authtoken.models import Token
1418
from django.utils import timezone
1519
from django.conf import settings
1620
from django.http import HttpRequest
@@ -870,3 +874,99 @@ def list(self, request):
870874

871875
serializer = self.serializer_class(results, many=True)
872876
return Response(serializer.data)
877+
878+
879+
class GetApiKeyView(APIView):
880+
permission_classes = (IsAuthenticated,)
881+
authentication_classes = (CsrfExemptSessionAuthentication,)
882+
883+
def get(self, request):
884+
token, created = Token.objects.get_or_create(user=request.user)
885+
return Response({'api_key': token.key })
886+
887+
888+
class SlicerApiVersionView(APIView):
889+
authentication_classes = [authentication.TokenAuthentication]
890+
permission_classes = (IsAuthenticated,)
891+
892+
def get(self, request, format=None):
893+
return Response({"text": "Obico"})
894+
895+
896+
class SlicerApiPrinterListView(APIView):
897+
authentication_classes = [authentication.TokenAuthentication]
898+
permission_classes = (IsAuthenticated,)
899+
900+
def get(self, request, format=None):
901+
return Response({"printers": [{"id": str(p.id), "name": p.name} for p in request.user.printer_set.all()]})
902+
903+
904+
class SlicerApiUploadView(APIView):
905+
authentication_classes = [authentication.TokenAuthentication]
906+
permission_classes = (IsAuthenticated,)
907+
908+
def post(self, request, format=None):
909+
data = dict(**request.data)
910+
file = request.data.get('file')
911+
if file:
912+
data['filename'] = file.name
913+
914+
serializer = GCodeFileDeSerializer(data=data, context={'request': request})
915+
serializer.is_valid(raise_exception=True)
916+
validated_data = serializer.validated_data
917+
918+
start_print = request.data.get('print') == 'true'
919+
920+
printer = None
921+
if start_print:
922+
raw_printer_id = request.data.get('printer_id')
923+
if raw_printer_id:
924+
try:
925+
printer_id = int(re.match(r".*\[(\d+)\]", raw_printer_id).groups()[0])
926+
except (ValueError, TypeError, IndexError):
927+
raise ValidationError({'printer_id': 'invalid value'})
928+
929+
printer = request.user.printer_set.filter(id=printer_id).first()
930+
if printer is None:
931+
raise ValidationError({'printer_id': 'could not find printer'})
932+
933+
if 'file' in request.FILES:
934+
file_size_limit = 500 * 1024 * 1024 if request.user.is_pro else 50 * 1024 * 1024
935+
num_bytes=request.FILES['file'].size
936+
if num_bytes > file_size_limit:
937+
return Response({'error': 'File size too large'}, status=413)
938+
939+
gcode_file = GCodeFile.objects.create(**validated_data)
940+
941+
self.set_metadata(gcode_file, *gcode_metadata.parse(request.FILES['file'], num_bytes, request.encoding or settings.DEFAULT_CHARSET))
942+
943+
request.FILES['file'].seek(0)
944+
_, ext_url = save_file_obj(self.path_in_storage(gcode_file), request.FILES['file'], settings.GCODE_CONTAINER)
945+
gcode_file.url = ext_url
946+
gcode_file.num_bytes = num_bytes
947+
gcode_file.save()
948+
949+
if start_print:
950+
# FIXME
951+
return Response({"message": "print queued"})
952+
953+
954+
return Response({"message": "uploaded successfully"})
955+
956+
raise ValidationError({"file": "Missing file body"})
957+
958+
def set_metadata(self, gcode_file, metadata, thumbnails):
959+
gcode_file.metadata_json = json.dumps(metadata)
960+
for key in ['estimated_time', 'filament_total']:
961+
setattr(gcode_file, key, metadata.get(key))
962+
963+
thumb_num = 0
964+
for thumb in sorted(thumbnails, key=lambda x: x.getbuffer().nbytes, reverse=True):
965+
thumb_num += 1
966+
if thumb_num > 3:
967+
continue
968+
_, ext_url = save_file_obj(f'gcode_thumbnails/{gcode_file.user.id}/{gcode_file.id}/{thumb_num}.png', thumb, settings.TIMELAPSE_CONTAINER)
969+
setattr(gcode_file, f'thumbnail{thumb_num}_url', ext_url)
970+
971+
def path_in_storage(self, gcode_file):
972+
return f'{gcode_file.user.id}/{gcode_file.id}'

backend/config/settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def get_bool(key, default):
6060
'simple_history',
6161
'widget_tweaks',
6262
'rest_framework',
63+
'rest_framework.authtoken',
6364
'jstemplate',
6465
'pushbullet',
6566
'corsheaders',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<template>
2+
<section>
3+
Your API Key is
4+
<code>{{ apiKey }}</code>
5+
</section>
6+
</template>
7+
8+
<script>
9+
10+
import axios from 'axios'
11+
import urls from '@config/server-urls'
12+
13+
export default {
14+
name: 'ApiKeySettings',
15+
16+
data() {
17+
return {
18+
apiKey: "",
19+
}
20+
},
21+
22+
created() {
23+
axios
24+
.get(urls.getApiKey())
25+
.then((response) => {
26+
this.apiKey = response.data.api_key
27+
})
28+
.catch((error) => {
29+
this.errorDialog(error, 'Failed to fetch api key')
30+
})
31+
},
32+
}
33+
</script>

frontend/src/config/server-urls.js

+2
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,6 @@ export default {
4848
gcodeFileBulkDelete: () => '/api/v1/g_code_files/bulk_delete/',
4949
gcodeFolderBulkMove: () => '/api/v1/g_code_folders/bulk_move/',
5050
gcodeFileBulkMove: () => '/api/v1/g_code_files/bulk_move/',
51+
52+
getApiKey: () => '/api/v1/apikey/',
5153
}

frontend/src/config/user-preferences/routes.js

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const defaultRoutes = {
44
GeneralPreferences: '/user_preferences/general/',
55
ThemePreferences: '/user_preferences/personalization/',
66
ProfilePreferences: '/user_preferences/profile/',
7+
ApiKey: '/user_preferences/api_key/',
78
AuthorizedApps: '/user_preferences/authorized_apps/',
89
GeneralNotifications: '/user_preferences/general_notifications/',
910
PushNotifications: '/user_preferences/mobile_push_notifications/',

frontend/src/config/user-preferences/sections.js

+7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ const defaultSections = {
2727
route: routes.ProfilePreferences,
2828
isHidden: onlyNotifications(),
2929
},
30+
ApiKey: {
31+
title: 'Api Key',
32+
faIcon: 'fas fa-check-circle',
33+
importComponent: () => import('@src/components/user-preferences/ApiKey'),
34+
route: routes.ApiKey,
35+
isHidden: onlyNotifications(),
36+
},
3037
AuthorizedApps: {
3138
title: 'Authorized Apps',
3239
faIcon: 'fas fa-check-circle',

0 commit comments

Comments
 (0)