Open13

[TIL] Django REST framework の学習メモ

Satoshi HasegawaSatoshi Hasegawa

適当にプロジェクトとアプリを作る。

django-admin startproject backend
cd backend/
python manage.py startapp monitoring

お決まり通りに INSTALLED_APPS を更新する。

DATABASES は SQLite を使うのでそのまま。

TIME_ZONEAsia/Tokyo に変更。

Satoshi HasegawaSatoshi Hasegawa

models.py にモデルを追加していくと肥大化するので分割する。

https://qiita.com/RyoMa_0923/items/c4ca5bd070e823403fdf
https://jpn.itlibra.com/article?id=20936

こんな感じでモデルを定義した。

class Sensor(models.Model):
    serial_number = models.CharField(
        verbose_name="S/N",
        primary_key=True,
        max_length=64,
        blank=False,
        null=False,
        validators=[validators.RegexValidator(
            regex='^[0-9a-zA-Z-]+$',
            message='numeric, alphabet or hylien'
        )]
    )
    display_name = models.CharField(
        verbose_name="Display Name",
        max_length=128,
        blank=True,
        null=False
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.serial_number

管理画面から編集できるように admin.py に追加する。

from django.contrib import admin
from monitoring.models import Sensor

admin.site.register(Sensor)

実行してみる。

python manage.py makemigrations monitoring
python manage.py migrate
python manage.py runserver

これで、管理顔面からデータを CURD できるようになった。

Satoshi HasegawaSatoshi Hasegawa

同様に、こんな感じでモデルを追加。

class SensingData(models.Model):
    sensor = models.ForeignKey(
        Sensor,
        to_field='serial_number',
        on_delete=models.CASCADE
    )
    timestamp = models.DateTimeField(default=datetime.now)
    tempareture = models.FloatField()
    humidity = models.FloatField()
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f'{self.sensor}_{self.timestamp.isoformat()}'
Satoshi HasegawaSatoshi Hasegawa

シリアライザはこんな感じ。チュートリアルを読むと、実際にどんな処理が行われているのかがよくわかる。

class SensorSerializer(serializers.ModelSerializer):
    class Meta:
        model = Sensor
        fields = ['serial_number', 'display_name', 'created_at', 'updated_at']


class SensingDataSerializer(serializers.ModelSerializer):
    class Meta:
        model = SensingData
        fields = ['sensor', 'timestamp',
                  'tempareture', 'humidity', 'created_at']
Satoshi HasegawaSatoshi Hasegawa

View を作る。 /sensor および /sensor/{pk} に対して CRUD できるようにするには、以下のようなコードになる。

@csrf_exempt
def sensor_list(request):
    if request.method == 'GET':
        sensor = Sensor.objects.all()
        serializeer = SensorSerializer(sensor, many=True)
        return JsonResponse(serializeer.data, safe=False)

    elif request.method == 'POST':
        data = JSONParser().parse(request)
        serializer = SensorSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=201)
        return JsonResponse(serializer.errors, status=400)


@csrf_exempt
def sensor_detail(request, pk):
    try:
        sensor = Sensor.objects.get(pk=pk)
    except:
        return HttpResponse(status=404)

    if request.method == 'GET':
        serializer = SensorSerializer(sensor)
        return JsonResponse(serializer.data)

    elif request.method == 'PUT':
        data = JSONParser().parse(request)
        serializer = SensorSerializer(sensor, data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data)
        return JsonResponse(serializer.erros, status=400)

    elif request.method == 'DELETE':
        sensor.delete()
        return HttpResponse(status=204)

urls.py を設定。プライマリーキーを数値ではなく文字列にしたので、 int ではなく slug になる。

urlpatterns = [
    path('sensor', views.sensor_list),
    path('sensor/<slug:pk>', views.sensor_detail)
]

プロジェクトの方の urls.py も更新する。

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('monitoring.urls')),
]
Satoshi HasegawaSatoshi Hasegawa

CURD してみる。

import requests
import json
import pprint

url = "http://localhost:8000/sensor/"

serial_number = 'BMTL-123456'
display_name1 = 'テスト機'
display_name2 = u'テスト機(改)'


headers = {
    'Content-Type': 'application/json',
    'Accept': 'application/json'
}


# Create a new sensor
print('## Create a new sensor')
response = requests.request(
    "POST", url, headers=headers, data=json.dumps({
        'serial_number': serial_number,
        'display_name': display_name1
    }))
print(f'Status Code = {response.status_code}')
pprint.pprint(response.json())
print('')

# Get a list of sensors
print('## Create a sensor list')
response = requests.request("GET", url, headers=headers)
print(f'Status Code = {response.status_code}')
pprint.pprint(response.json())
print('')

# Get sensor details
print('## Create sensor details')
response = requests.request("GET", url + serial_number, headers=headers)
print(f'Status Code = {response.status_code}')
pprint.pprint(response.json())
print('')

# Update sensor details
print('## Update the sensor details')
response = requests.request(
    "PUT", url + serial_number, headers=headers, data=json.dumps({
        'serial_number': serial_number,
        'display_name': display_name2
    }))
print(f'Status Code = {response.status_code}')
pprint.pprint(response.json())
print('')

# delete the sensor
print('## Delete the sensor')
response = requests.request("DELETE", url + serial_number, headers=headers)
print(f'Status Code = {response.status_code}')
pprint.pprint(response.text)
print('')

実行結果

$ python 001_crud_sensor.py 
## Create a new sensor
Status Code = 201
{'created_at': '2021-07-22T13:58:22.741190+09:00',
 'display_name': 'テスト機',
 'serial_number': 'BMTL-123456',
 'updated_at': '2021-07-22T13:58:22.741264+09:00'}

## Create a sensor list
Status Code = 200
[{'created_at': '2021-07-21T23:40:32.587705+09:00',
  'display_name': '一号機',
  'serial_number': 'ADX-123987',
  'updated_at': '2021-07-21T23:40:32.587741+09:00'},
 {'created_at': '2021-07-22T13:28:25.635808+09:00',
  'display_name': '二号機(改)',
  'serial_number': 'DMT-774321',
  'updated_at': '2021-07-22T13:28:25.635885+09:00'},
 {'created_at': '2021-07-22T13:32:41.579019+09:00',
  'display_name': '二号機(改)',
  'serial_number': 'DMT-774322',
  'updated_at': '2021-07-22T13:32:41.579073+09:00'},
 {'created_at': '2021-07-22T13:35:11.876769+09:00',
  'display_name': '二号機(改)',
  'serial_number': 'DMT-774323',
  'updated_at': '2021-07-22T13:35:11.876807+09:00'},
 {'created_at': '2021-07-22T13:58:22.741190+09:00',
  'display_name': 'テスト機',
  'serial_number': 'BMTL-123456',
  'updated_at': '2021-07-22T13:58:22.741264+09:00'}]

## Create sensor details
Status Code = 200
{'created_at': '2021-07-22T13:58:22.741190+09:00',
 'display_name': 'テスト機',
 'serial_number': 'BMTL-123456',
 'updated_at': '2021-07-22T13:58:22.741264+09:00'}

## Update the sensor details
Status Code = 200
{'created_at': '2021-07-22T13:58:22.741190+09:00',
 'display_name': 'テスト機(改)',
 'serial_number': 'BMTL-123456',
 'updated_at': '2021-07-22T13:58:22.800639+09:00'}

## Delete the sensor
Status Code = 204
''
Satoshi HasegawaSatoshi Hasegawa

APIView クラスを使うとこんな感じになる。

class SensorList(APIView):
    def get(self, request, format=None):
        sensor = Sensor.objects.all()
        serializeer = SensorSerializer(sensor, many=True)
        return Response(serializeer.data)

    def post(self, request, format=None):
        serializer = SensorSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class SensorDetail(APIView):
    def get_object(self, pk):
        try:
            return Sensor.objects.get(pk=pk)
        except Sensor.DoesNotExist:
            raise Http404

    def get(self, request, pk, format=None):
        sensor = self.get_object(pk)
        serializeer = SensorSerializer(sensor)
        return Response(serializeer.data)

    def put(self, request, pk, format=None):
        sensor = self.get_object(pk)
        serializer = SensorSerializer(sensor, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.erros, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk, format=None):
        sensor = self.get_object(pk)
        sensor.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
Satoshi HasegawaSatoshi Hasegawa

Viewset を使うと、普通に CRUD するだけなら、たったこれだけのコードになってしまう。

class SensorViewSet(viewsets.ModelViewSet):
    queryset = Sensor.objects.all()
    serializer_class = SensorSerializer

queryset = Sensor.objects.all() となっているが、detail の時も、PK を使わずに、DBからは全件データを取ってきてしまっているのだろうか?(さすがに、そんなことはしないと思うのだが…。)
→ 発行された SQL を確認したら、ちゃんと SELECT ••• FROM "monitoring_sensor" WHERE "monitoring_sensor"."serial_number" = '''BMTL-123456''' LIMIT 21 となっていた。

Satoshi HasegawaSatoshi Hasegawa

今度は /sensor/{pk}/data/ で、そのセンサーのデータを登録・取得したい。その場合は、 @action を使うらしい。

class SensorViewSet(viewsets.ModelViewSet):
    queryset = Sensor.objects.all()
    serializer_class = SensorSerializer

    @action(detail=True, methods=['GET', 'POST'])
    def data(self, request, *args, **kwargs):
    ... (snip) ...
  • メソッド : request.method
  • Body : request.data
  • PK : kwargs['pk']
  • パラメータ : request.GET.get('val')
Satoshi HasegawaSatoshi Hasegawa

POST でデータを投入する。

def post_data(payload, pk):
    data = payload
    data['sensor'] = pk
    if 'timestamp' not in data:
        data['timestamp'] = datetime.now().astimezone(tz=JST).isoformat()

    serializer = SensingDataSerializer(data=data)
    if serializer.is_valid():
        serializer.save()
        return Response(serializer.data, status=status.HTTP_201_CREATED)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
... (snip) ...

class SensorViewSet(viewsets.ModelViewSet):
    queryset = Sensor.objects.all()
    serializer_class = SensorSerializer

    @action(detail=True, methods=['GET', 'POST'])
    def data(self, request, *args, **kwargs):
        if request.method == 'GET':
            pass

        elif request.method == 'POST':
            return post_data(request.data, kwargs['pk'])

        return Response({}, status=status.HTTP_400_BAD_REQUEST)
Satoshi HasegawaSatoshi Hasegawa

GET でセンサーデータを取得できるようにする。パラメータで範囲指定できるようにする。省略された場合は、現在時刻から1時間前までの範囲。

/sensor/{pk}/data/?from=<エポック秒>&to=<エポック秒>

コードはこんな感じになる。

def now():
    return int(datetime.now(timezone.utc).strftime('%s'))


def epoch_to_iso_jst(epoch):
    try:
        v = int(epoch)
    except Exception:
        v = now()
    return datetime.fromtimestamp(v).replace(tzinfo=timezone.utc).astimezone(tz=JST).isoformat()


def search_data(request, pk):
    try:
        sensor = Sensor.objects.get(pk=pk)
    except Sensor.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    try:
        from_dt = int(request.GET.get('from'))
    except Exception:
        from_dt = now() - 3600
    finally:
        from_dt = epoch_to_iso_jst(from_dt)

    try:
        to_dt = int(request.GET.get('to'))
    except Exception:
        to_dt = now()
    finally:
        to_dt = epoch_to_iso_jst(to_dt)

    data = SensingData.objects.filter(
        sensor=sensor, timestamp__gte=from_dt, timestamp__lte=to_dt)
    serializer = SensingDataSerializer(data, many=True)
    return Response(serializer.data)

... (snip) ...

    @ action(detail=True, methods=['GET', 'POST'])
    def data(self, request, *args, **kwargs):
        if request.method == 'GET':
            return search_data(request, kwargs['pk'])
Satoshi HasegawaSatoshi Hasegawa

センサーの一覧を Web 画面に表示する場合。

views.py
def sensor_list(request):
    if request.method == 'GET':
        sensor = Sensor.objects.all()
        serializer = SensorSerializer(sensor, many=True)
        print(serializer.data)
        content = {'sensor_list': json.loads(json.dumps(serializer.data))}
        return render(request, 'dashboard/sensor_list.html', content)
sensor_list.html
<div class="sample">{{ sensor_list }}</div>

単純に JSON が表示されるので、あとは装飾する。