Another set of changes

This commit is contained in:
Kumi 2020-10-08 19:58:47 +02:00
parent ed97894891
commit ff97a4845b
10 changed files with 791 additions and 11 deletions

3
.gitignore vendored
View file

@ -2,4 +2,5 @@
__pycache__/
migrations/
*.swp
db.sqlite3
db.sqlite3
customsettings.py

View file

@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand, CommandError
from core.models import APIKey
class Command(BaseCommand):
help = 'Add new API key'
def add_arguments(self, parser):
parser.add_argument('--description', "-d", required=False, type=str, default="")
def handle(self, *args, **options):
key = APIKey.objects.create(description=options["description"])
self.stdout.write(self.style.SUCCESS(f'Successfully created key "{ key.key }"'))

View file

@ -1,4 +1,4 @@
from django.db.models import Model, UUIDField, CharField, TextField, ForeignKey, CASCADE, PositiveIntegerField
from django.db.models import Model, UUIDField, CharField, TextField, ForeignKey, CASCADE, PositiveIntegerField, SET_NULL
import uuid
import string
@ -72,13 +72,17 @@ class Code(Model):
if not self.order:
self.order = max([code.order for code in self.series.code_set.all()] + [0]) + 1
class Scan(Model):
uuid = UUIDField()
code = ForeignKey(Code, CASCADE)
class Content(Model):
literal = TextField()
code = ForeignKey(Code, SET_NULL, null=True)
class Media(Model):
uuid = UUIDField(primary_key=True)
code = ForeignKey(Content, CASCADE)
@classmethod
def generate_uuid(cls):
uuid = uuid.uuid4(primary_key=True)
id = uuid.uuid4()
try:
cls.objects.get(uuid=id)
except cls.DoesNotExist:
@ -88,4 +92,22 @@ class Scan(Model):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.uuid:
self.uuid = self.generate_uuid()
self.uuid = self.generate_uuid()
class APIKey(Model):
key = UUIDField(primary_key=True)
description = CharField(max_length=256, blank=True, null=True)
@classmethod
def generate_uuid(cls):
id = uuid.uuid4()
try:
cls.objects.get(key=id)
except cls.DoesNotExist:
return id
return cls.generate_uuid()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.key:
self.key = self.generate_uuid()

20
core/urls/__init__.py Normal file
View file

@ -0,0 +1,20 @@
"""expqr URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.urls import path, include
urlpatterns = [
path('api/', include('core.urls.api'))
]

27
core/urls/api.py Normal file
View file

@ -0,0 +1,27 @@
"""expqr URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.urls import path
from core.views import APICreateSeries, APISeriesByIDDispatcher, APICodesBySeriesIDDispatcher, APICreateCodes, APICodeByID
urlpatterns = [
path('series/', APICreateSeries.as_view()),
path('series/<id>/', APISeriesByIDDispatcher.as_view()),
path('series/<id>/codes/', APICodesBySeriesIDDispatcher.as_view()),
path('codes/', APICreateCodes.as_view()),
path('codes/<uuid>/', APICodeByID.as_view()),
path('codes/<seriesid>/<codeid>/', APICodeByID.as_view()),
]

View file

@ -1,3 +1,236 @@
from django.views import View
from django.http import JsonResponse
from django.forms.models import model_to_dict
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
import json
import uuid
from PIL import Image
from io import BytesIO
import urllib.request
from core.models import Series, APIKey, Code
class APIView(View):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
try:
APIKey.objects.get(key=request.headers["apikey"])
except APIKey.DoesNotExist:
return JsonResponse({"error": "No valid API key provided"})
return super().dispatch(request, *args, **kwargs)
class JsonView(APIView):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
try:
self.data = json.loads(request.body.decode("utf-8"))
except Exception:
return JsonResponse({"error": "No valid json found"})
return super().dispatch(request, *args, **kwargs)
class APICreateSeries(JsonView):
def post(self, request, *args, **kwargs):
try:
title = self.data["title"]
except KeyError:
return JsonResponse({"error": "No title for Series provided"})
description = self.data.get("description")
id = self.data.get("id")
uuid = self.data.get("uuid")
if id:
try:
Series.objects.get(id=id)
raise ValueError("Series object with id %s exists" % id)
except Series.DoesNotExist:
pass
if uuid:
try:
Series.objects.get(id=uuid)
raise ValueError("Series object with uuid %s exists" % uuid)
except Series.DoesNotExist:
pass
series = Series.objects.create(uuid=uuid, id=id, title=title, description=description)
return JsonResponse(model_to_dict(series))
class SeriesAPIView(APIView):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
try:
try:
is_uuid = bool(uuid.UUID(kwargs["id"]))
except:
is_uuid = False
if is_uuid:
self.series = Series.objects.get(uuid=kwargs["id"])
else:
self.series = Series.objects.get(id=kwargs["id"])
except Series.DoesNotExist:
return JsonResponse({"error": "No Series with ID %s found" % kwargs["id"]})
return super().dispatch(request, *args, **kwargs)
class APISeriesByIDDispatcher(SeriesAPIView):
def get(self, request, *args, **kwargs):
return JsonResponse(model_to_dict(self.series))
def post(self, request, *args, **kwargs):
try:
data = json.loads(request.body.decode("utf-8"))
except Exception:
return JsonResponse({"error": "No valid json found"})
title = data.get("title", self.series.title)
description = data.get("description", self.series.description)
id = data.get("id", self.series.id)
uuid = data.get("uuid", self.series.uuid)
if id != self.series.id:
return JsonResponse({"error": "Cannot change Series ID"})
if uuid != self.series.uuid:
return JsonResponse({"error": "Cannot change Series UUID"})
self.series.title = title
self.series.description = description
self.series.save()
return JsonResponse(model_to_dict(self.series))
def delete(self, request, *args, **kwargs):
self.series.delete()
return JsonResponse({})
class APICodesBySeriesIDDispatcher(SeriesAPIView):
def get(self, request, *args, **kwargs):
return JsonResponse(list(self.series.code_set.values()), safe=False)
def post(self, request, *args, **kwargs):
try:
data = json.loads(request.body.decode("utf-8"))
except Exception:
return JsonResponse({"error": "No valid json found"})
objects = []
for spec in data:
title = spec["title"]
id = spec.get("id")
uuid = spec.get("uuid")
order = 0
try:
code = Code(uuid=uuid, id=id, title=title, series=self.series, order=order)
code.save()
objects.append(model_to_dict(code))
except Exception as e:
for o in objects:
o.delete()
return JsonResponse({"error": "Failed creating Code objects: %s" % e})
return JsonResponse(objects, safe=False)
class APICreateCodes(APIView):
def post(self, request, *args, **kwargs):
try:
data = json.loads(request.body.decode("utf-8"))
except Exception:
return JsonResponse({"error": "No valid json found"})
objects = []
for spec in data:
title = spec["title"]
id = spec.get("id")
nuuid = spec.get("uuid")
series_id = spec["series"]
order = 0
try:
is_uuid = bool(uuid.UUID(series_id))
except:
is_uuid = False
if is_uuid:
series = Series.objects.get(uuid=series_id)
else:
series = Series.objects.get(id=series_id)
try:
code = Code(uuid=nuuid, id=id, title=title, series=series, order=order)
code.save()
objects.append(model_to_dict(code))
except Exception as e:
for o in objects:
o.delete()
return JsonResponse({"error": "Failed creating Code objects: %s" % e})
return JsonResponse(objects, safe=False)
class CodeAPIView(APIView):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
try:
if kwargs.get("uuid"):
self.code = Code.objects.get(uuid=kwargs.get("uuid"))
else:
self.code = Code.objects.get(id=kwargs["codeid"], series=Series.objects.get(id=kwargs["seriesid"]))
except Code.DoesNotExist:
return JsonResponse({"error": "No Code for this definition found"})
return super().dispatch(request, *args, **kwargs)
class APICodeByID(CodeAPIView):
def get(self, request, *args, **kwargs):
return JsonResponse(model_to_dict(self.code))
def post(self, request, *args, **kwargs):
try:
data = json.loads(request.body.decode("utf-8"))
except Exception:
return JsonResponse({"error": "No valid json found"})
title = data.get("title", self.code.title)
id = data.get("id", self.code.id)
uuid = data.get("uuid", self.code.uuid)
if id != self.code.id:
return JsonResponse({"error": "Cannot change Code ID"})
if uuid != self.code.uuid:
return JsonResponse({"error": "Cannot change Code UUID"})
self.code.title = title
self.code.save()
return JsonResponse(model_to_dict(self.code))
def delete(self, request, *args, **kwargs):
self.code.delete()
return JsonResponse({})
class APIAnalyzeContent(APIView):
def post(self, request, *args, **kwargs):
url = request.body.decode("utf-8")
try:
val = URLValidator().val(url)
except ValidationError:
return JsonResponse({"error": "Failed to validate URL"})
content = BytesIO(urllib.request.urlopen('url').read())

View file

@ -7,4 +7,5 @@ SECRET_KEY = 'me98p&0-ijj-*vov@vm_&z&x#gr9uvvc9*y$n!!%=+javz^-#7'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
ALLOWED_HOSTS = []

421
doc/swagger.yaml Normal file
View file

@ -0,0 +1,421 @@
openapi: 3.0.0
info:
description: API for handling EXP360 QR Code generation and analysis
version: "0.1"
title: EXPQR
contact:
email: support@kumi.email
tags:
- name: series
description: Operations on series of codes to be used in a single shoot
- name: code
description: Operations on individual QR codes
- name: content
description: Operations on content elements including QR codes
paths:
/series:
post:
tags:
- series
summary: Create a new code series
description: ""
operationId: addSeries
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Series"
description: "(Partial) `Series` object to be created, required field: `title`"
required: true
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Series"
"/series/{id}":
get:
tags:
- series
summary: Find Series by ID or UUID
description: Returns a single Series object
operationId: getSeriesByID
parameters:
- name: id
in: path
description: ID or UUID of series to return
required: true
schema:
type: string
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Series"
"400":
description: Invalid ID supplied
"404":
description: Series not found
post:
tags:
- series
summary: Modifies an existing Series object
description: ""
operationId: modifySeriesByID
parameters:
- name: id
in: path
description: ID or UUID of `Series` object to be modified
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Series"
description: "Partial `Series` object, allowed fields: `title`, `description`"
required: true
responses:
"405":
description: Invalid input
delete:
tags:
- series
summary: Deletes a Series object
description: ""
operationId: deleteSeries
parameters:
- name: id
in: path
description: ID or UUID of `Series` object to delete
required: true
schema:
type: string
responses:
"400":
description: Invalid ID supplied
"404":
description: Series not found
"/series/{id}/codes":
get:
tags:
- series
summary: Return all QR codes associated with Series
parameters:
- name: id
in: path
description: ID or UUID of `Series` object to return codes for
required: true
schema:
type: string
responses:
200:
description: List of `Code` objects associated with `Series`
post:
tags:
- series
summary: creates new QR codes for Series
description: ""
operationId: createCodes
parameters:
- name: id
in: path
description: ID or UUID of the `Series` object to create codes for
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Code"
description: (Partial) `Code` objects to be created, forbidden parameter: `series`
required: true
responses:
"200":
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Code"
"/code/":
post:
tags:
- code
summary: create new Code objects
requestBody:
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Code"
responses:
"200":
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Code"
"/code/{uuid}":
get:
tags:
- code
summary: returns a Code object by its UUID
operationId: getCodeByUUID
parameters:
- name: uuid
in: path
description: UUID of the `Code` object to return
required: true
schema:
type: string
format: uuid
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Code"
delete:
tags:
- code
summary: delete a Code object by its UUID
operationId: deleteCodeByUUID
parameters:
- name: uuid
in: path
description: UUID of the `Code` object to delete
required: true
schema:
type: string
format: uuid
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Code"
post:
tags:
- code
summary: modify a Code object by its UUID
operationId: modifyCodeByUUID
parameters:
- name: uuid
in: path
description: UUID of the `Code` object to modify
required: true
schema:
type: string
format: uuid
requestBody:
$ref: "#/components/requestBodies/modifyCodeByUUIDBody"
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Code"
"/code/{seriesid}/{codeid}":
get:
tags:
- code
summary: returns a Code object by its Series ID and Code ID
operationId: getCodeByID
parameters:
- name: seriesid
in: path
description: ID or UUID of the `Series` the `Code` object to return is part of
required: true
schema:
type: string
- name: codeid
in: path
description: ID of the `Code` object to return
required: true
schema:
type: string
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Code"
delete:
tags:
- code
summary: delete a Code object by its Series ID and Code ID
operationId: deleteCodeByID
parameters:
- name: seriesid
in: path
description: ID or UUID of the `Series` the `Code` object to delete is part of
required: true
schema:
type: string
- name: codeid
in: path
description: ID of the `Code` object to delete
required: true
schema:
type: string
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Code"
post:
tags:
- code
summary: modify a Code object by its Series ID and Code ID
operationId: modifyCodeByID
parameters:
- name: seriesid
in: path
description: ID or UUID of the `Series` the `Code` object to modify is part of
required: true
schema:
type: string
- name: codeid
in: path
description: ID of the `Code` object to modify
required: true
schema:
type: string
requestBody:
$ref: "#/components/requestBodies/modifyCodeByUUIDBody"
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Code"
/content:
post:
tags:
- content
summary: analyze new media item and return content
operationId: createContent
requestBody:
content:
application/json:
schema:
url:
type: string
uuid:
type: string
format: uuid
required: true
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Media"
"/content/{uuid}":
get:
tags:
- content
summary: fetch details on media item by UUID
parameters:
- name: uuid
in: path
description: UUID of the `Media` item to retrieve
schema:
type: string
required: true
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Media"
security:
- api_key: []
servers:
- url: https://expqr.kumi.live/api
components:
requestBodies:
modifyCodeByUUIDBody:
content:
application/json:
schema:
type: string
description: New title of the `Code` object
required: true
securitySchemes:
api_key:
type: apiKey
name: api_key
in: header
schemas:
Series:
type: object
required:
- title
properties:
title:
type: string
description:
type: string
id:
type: string
uuid:
type: string
format: uuid
Code:
type: object
required:
- title
properties:
title:
type: string
id:
type: string
uuid:
type: string
format: uuid
series:
$ref: "#/components/schemas/Series"
Media:
type: object
required:
- uuid
properties:
uuid:
type: string
format: uuid
content:
$ref: "#/components/schemas/Content"
Content:
type: object
required:
- literal
properties:
literal:
type: string
code:
$ref: "#/components/schemas/Code"

View file

@ -13,9 +13,8 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('core.urls')),
]

View file

@ -0,0 +1,43 @@
import zxing
import pyzbar.pyzbar
import PIL.Image
import PIL.ImageEnhance
import PIL.ImageFilter
import sys
import tempfile
class QRCode:
def __init__(self, content, rect):
self.content = content
self.rect = rect
def read_code(imagepath):
image = PIL.Image.open(imagepath)
reader = zxing.BarCodeReader()
codes = []
for _ in range(10):
tempimage = tempfile.NamedTemporaryFile()
image.save(tempimage, format="png")
zxcontent = reader.decode(tempimage.name)
zbcontent = pyzbar.pyzbar.decode(PIL.Image.open(tempimage.name))
content = []
if zxcontent:
content.append(zxcontent.raw)
for single in zbcontent:
content.append(single.data.decode())
for code in content:
if not code in codes:
codes.append(code)
if codes:
return codes
image = PIL.ImageEnhance.Contrast(image).enhance(2.5)
if __name__ == "__main__":
content = read_code(sys.argv[1])
print(content)