| """REST API for accessing the crashreports statistics.""" |
| import zipfile |
| |
| from rest_framework import generics |
| from rest_framework import serializers |
| from rest_framework.exceptions import NotFound |
| from rest_framework.response import Response |
| from rest_framework.views import APIView |
| |
| from django.core.exceptions import ObjectDoesNotExist |
| from django.db import connection |
| from django.db.models.expressions import F |
| |
| from django_filters.rest_framework import ( |
| DjangoFilterBackend, |
| DateFilter, |
| FilterSet, |
| CharFilter, |
| BooleanFilter, |
| ) |
| |
| from crashreport_stats.models import ( |
| Version, |
| VersionDaily, |
| RadioVersion, |
| RadioVersionDaily, |
| ) |
| from crashreports.models import Device, Crashreport, HeartBeat, LogFile |
| from crashreports.permissions import ( |
| HasRightsOrIsDeviceOwnerDeviceCreation, |
| HasStatsAccess, |
| ) |
| from . import raw_querys |
| |
| |
| def dictfetchall(cursor): |
| """Return all rows from a cursor as a dict.""" |
| desc = cursor.description |
| return [ |
| dict(zip([col[0] for col in desc], row)) for row in cursor.fetchall() |
| ] |
| |
| |
| class DeviceUpdateHistory(APIView): |
| """View the update history of a specific device.""" |
| |
| permission_classes = (HasRightsOrIsDeviceOwnerDeviceCreation,) |
| |
| def get(self, request, uuid, format=None): |
| """Get the update history of a device. |
| |
| Args: |
| request: Http request |
| uuid: The UUID of the device |
| format: Optional response format parameter |
| |
| Returns: The update history of the requested device. |
| |
| """ |
| cursor = connection.cursor() |
| raw_querys.execute_device_update_history_query(cursor, {"uuid": uuid}) |
| res = dictfetchall(cursor) |
| return Response(res) |
| |
| |
| class DeviceReportHistory(APIView): |
| """View the report history of a specific device.""" |
| |
| permission_classes = (HasRightsOrIsDeviceOwnerDeviceCreation,) |
| |
| def get(self, request, uuid, format=None): |
| """Get the report history of a device. |
| |
| Args: |
| request: Http request |
| uuid: The UUID of the device |
| format: Optional response format parameter |
| |
| Returns: The report history of the requested device. |
| |
| """ |
| cursor = connection.cursor() |
| raw_querys.execute_device_report_history(cursor, {"uuid": uuid}) |
| res = dictfetchall(cursor) |
| return Response(res) |
| |
| |
| class Status(APIView): |
| """View the number of devices, crashreports and heartbeats.""" |
| |
| permission_classes = (HasStatsAccess,) |
| |
| def get(self, request, format=None): |
| """Get the number of devices, crashreports and heartbeats. |
| |
| Args: |
| request: Http request |
| format: Optional response format parameter |
| |
| Returns: The number of devices, crashreports and heartbeats. |
| |
| """ |
| num_devices = Device.objects.count() |
| num_crashreports = Crashreport.objects.count() |
| num_heartbeats = HeartBeat.objects.count() |
| return Response( |
| { |
| "devices": num_devices, |
| "crashreports": num_crashreports, |
| "heartbeats": num_heartbeats, |
| } |
| ) |
| |
| |
| class DeviceStat(APIView): |
| """View an overview of the statistics of a device.""" |
| |
| permission_classes = (HasRightsOrIsDeviceOwnerDeviceCreation,) |
| |
| def get(self, request, uuid, format=None): |
| """Get some general statistics for a device. |
| |
| Args: |
| request: Http request |
| uuid: The UUID of the device |
| format: Optional response format parameter |
| |
| Returns: Some general information of the device in a dictionary. |
| |
| """ |
| device = Device.objects.filter(uuid=uuid) |
| last_active = ( |
| HeartBeat.objects.filter(device=device).order_by("-date")[0].date |
| ) |
| heartbeats = HeartBeat.objects.filter(device=device).count() |
| crashreports = ( |
| Crashreport.objects.filter(device=device) |
| .filter(boot_reason__in=Crashreport.CRASH_BOOT_REASONS) |
| .count() |
| ) |
| crashes_per_day = ( |
| crashreports * 1.0 / heartbeats if heartbeats > 0 else 0 |
| ) |
| smpls = ( |
| Crashreport.objects.filter(device=device) |
| .filter(boot_reason__in=Crashreport.SMPL_BOOT_REASONS) |
| .count() |
| ) |
| smpl_per_day = smpls * 1.0 / heartbeats if heartbeats > 0 else 0 |
| return Response( |
| { |
| "uuid": uuid, |
| "last_active": last_active, |
| "heartbeats": heartbeats, |
| "crashreports": crashreports, |
| "crashes_per_day": crashes_per_day, |
| "smpls": smpls, |
| "smpl_per_day": smpl_per_day, |
| "board_date": device[0].board_date, |
| } |
| ) |
| |
| |
| class LogFileDownload(APIView): |
| """View for downloading log files.""" |
| |
| permission_classes = (HasRightsOrIsDeviceOwnerDeviceCreation,) |
| |
| def get(self, request, id_logfile, format=None): |
| """Get a logfile. |
| |
| Args: |
| request: Http request |
| id_logfile: The id of the log file |
| format: Optional response format parameter |
| |
| Returns: The log file with the corresponding id. |
| |
| """ |
| try: |
| logfile = LogFile.objects.get(id=id_logfile) |
| except ObjectDoesNotExist: |
| raise NotFound(detail="Logfile does not exist.") |
| zip_file = zipfile.ZipFile(logfile.logfile.path) |
| ret = {} |
| for file in zip_file.filelist: |
| file_open = zip_file.open(file) |
| ret[file.filename] = file_open.read() |
| return Response(ret) |
| |
| |
| class _VersionStatsFilter(FilterSet): |
| first_seen_before = DateFilter( |
| field_name="first_seen_on", lookup_expr="lte" |
| ) |
| first_seen_after = DateFilter(field_name="first_seen_on", lookup_expr="gte") |
| released_before = DateFilter(field_name="released_on", lookup_expr="lte") |
| released_after = DateFilter(field_name="released_on", lookup_expr="gte") |
| |
| |
| class _VersionStatsSerializer(serializers.ModelSerializer): |
| permission_classes = (HasStatsAccess,) |
| |
| |
| class _VersionStatsListView(generics.ListAPIView): |
| permission_classes = (HasStatsAccess,) |
| filter_backends = (DjangoFilterBackend,) |
| |
| |
| class _DailyVersionStatsFilter(FilterSet): |
| date_start = DateFilter(field_name="date", lookup_expr="gte") |
| date_end = DateFilter(field_name="date", lookup_expr="lte") |
| |
| |
| class _DailyVersionStatsSerializer(serializers.ModelSerializer): |
| permission_classes = (HasStatsAccess,) |
| |
| |
| class _DailyVersionStatsListView(generics.ListAPIView): |
| permission_classes = (HasStatsAccess,) |
| filter_backends = (DjangoFilterBackend,) |
| |
| |
| class VersionSerializer(_VersionStatsSerializer): |
| """Serializer for the Version class.""" |
| |
| class Meta: # noqa: D106 |
| model = Version |
| fields = "__all__" |
| |
| |
| class VersionFilter(_VersionStatsFilter): |
| """Filter for Version instances.""" |
| |
| class Meta: # noqa: D106 |
| model = Version |
| fields = "__all__" |
| |
| |
| class VersionListView(_VersionStatsListView): |
| """View for listing versions.""" |
| |
| queryset = Version.objects.all().order_by("-heartbeats") |
| filter_class = VersionFilter |
| serializer_class = VersionSerializer |
| |
| |
| class VersionDailyFilter(_DailyVersionStatsFilter): |
| """Filter for VersionDaily instances.""" |
| |
| version__build_fingerprint = CharFilter() |
| version__is_official_release = BooleanFilter() |
| version__is_beta_release = BooleanFilter() |
| |
| class Meta: # noqa: D106 |
| model = VersionDaily |
| fields = "__all__" |
| |
| |
| class VersionDailySerializer(_DailyVersionStatsSerializer): |
| """Serializer for VersionDaily instances.""" |
| |
| build_fingerprint = serializers.CharField() |
| |
| class Meta: # noqa: D106 |
| model = VersionDaily |
| fields = "__all__" |
| |
| |
| class VersionDailyListView(_DailyVersionStatsListView): |
| """View for listing VersionDaily instances.""" |
| |
| queryset = ( |
| VersionDaily.objects.annotate( |
| build_fingerprint=F("version__build_fingerprint") |
| ) |
| .all() |
| .order_by("date") |
| ) |
| filter_class = VersionDailyFilter |
| filter_fields = ( |
| "version__build_fingerprint", |
| "version__is_official_release", |
| "version__is_beta_release", |
| ) |
| serializer_class = VersionDailySerializer |
| |
| |
| class RadioVersionSerializer(_VersionStatsSerializer): |
| """Serializer for RadioVersion instances.""" |
| |
| class Meta: # noqa: D106 |
| model = RadioVersion |
| fields = "__all__" |
| |
| |
| class RadioVersionFilter(_VersionStatsFilter): |
| """Filter for RadioVersion instances.""" |
| |
| class Meta: # noqa: D106 |
| model = RadioVersion |
| fields = "__all__" |
| |
| |
| class RadioVersionListView(_VersionStatsListView): |
| """View for listing RadioVersion instances.""" |
| |
| queryset = RadioVersion.objects.all().order_by("-heartbeats") |
| serializer_class = RadioVersionSerializer |
| filter_class = RadioVersionFilter |
| |
| |
| class RadioVersionDailyFilter(_DailyVersionStatsFilter): |
| """Filter for RadioVersionDaily instances.""" |
| |
| version__radio_version = CharFilter() |
| version__is_official_release = BooleanFilter() |
| version__is_beta_release = BooleanFilter() |
| |
| class Meta: # noqa: D106 |
| model = RadioVersionDaily |
| fields = "__all__" |
| |
| |
| class RadioVersionDailySerializer(_DailyVersionStatsSerializer): |
| """Serializer for RadioVersionDaily instances.""" |
| |
| radio_version = serializers.CharField() |
| |
| class Meta: # noqa: D106 |
| model = RadioVersionDaily |
| fields = "__all__" |
| |
| |
| class RadioVersionDailyListView(_DailyVersionStatsListView): |
| """View for listing RadioVersionDaily instances.""" |
| |
| queryset = ( |
| RadioVersionDaily.objects.annotate( |
| radio_version=F("version__radio_version") |
| ) |
| .all() |
| .order_by("date") |
| ) |
| filter_class = RadioVersionDailyFilter |
| filter_fields = ( |
| "version__radio_version", |
| "version__is_official_release", |
| "version__is_beta_release", |
| ) |
| serializer_class = RadioVersionDailySerializer |