blob: 5db2e36e93f6b90cec550b5f6a7b8200784c78c6 [file] [log] [blame]
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +02001"""REST API for accessing the crashreports statistics."""
2import zipfile
3
Dirk Vogt62ff7f22017-05-04 16:07:21 +02004from rest_framework import generics
5from rest_framework import serializers
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +02006from rest_framework.exceptions import NotFound
Dirk Vogt62ff7f22017-05-04 16:07:21 +02007from rest_framework.response import Response
8from rest_framework.views import APIView
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +02009
10from django.core.exceptions import ObjectDoesNotExist
Dirk Vogt62ff7f22017-05-04 16:07:21 +020011from django.db import connection
Dirk Vogt1accb672017-05-10 14:07:42 +020012from django.db.models.expressions import F
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020013
14from django_filters.rest_framework import (
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020015 DjangoFilterBackend,
16 DateFilter,
17 FilterSet,
18 CharFilter,
19 BooleanFilter,
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020020)
21
22from crashreport_stats.models import (
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020023 Version,
24 VersionDaily,
25 RadioVersion,
26 RadioVersionDaily,
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020027)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020028from crashreports.models import Device, Crashreport, HeartBeat, LogFile
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020029from crashreports.permissions import (
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020030 HasRightsOrIsDeviceOwnerDeviceCreation,
31 HasStatsAccess,
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020032)
33from . import raw_querys
34
Dirk Vogt62ff7f22017-05-04 16:07:21 +020035
36def dictfetchall(cursor):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020037 """Return all rows from a cursor as a dict."""
Dirk Vogt62ff7f22017-05-04 16:07:21 +020038 desc = cursor.description
39 return [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020040 dict(zip([col[0] for col in desc], row)) for row in cursor.fetchall()
Dirk Vogt62ff7f22017-05-04 16:07:21 +020041 ]
42
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020043
Dirk Vogt62ff7f22017-05-04 16:07:21 +020044class DeviceUpdateHistory(APIView):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020045 """View the update history of a specific device."""
46
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020047 permission_classes = (HasRightsOrIsDeviceOwnerDeviceCreation,)
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020048
Dirk Vogt62ff7f22017-05-04 16:07:21 +020049 def get(self, request, uuid, format=None):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020050 """Get the update history of a device.
51
52 Args:
53 request: Http request
54 uuid: The UUID of the device
55 format: Optional response format parameter
56
57 Returns: The update history of the requested device.
58
59 """
Dirk Vogt62ff7f22017-05-04 16:07:21 +020060 cursor = connection.cursor()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020061 raw_querys.execute_device_update_history_query(cursor, {"uuid": uuid})
Dirk Vogt62ff7f22017-05-04 16:07:21 +020062 res = dictfetchall(cursor)
63 return Response(res)
64
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020065
Dirk Vogt62ff7f22017-05-04 16:07:21 +020066class DeviceReportHistory(APIView):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020067 """View the report history of a specific device."""
68
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020069 permission_classes = (HasRightsOrIsDeviceOwnerDeviceCreation,)
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020070
Dirk Vogt62ff7f22017-05-04 16:07:21 +020071 def get(self, request, uuid, format=None):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020072 """Get the report history of a device.
73
74 Args:
75 request: Http request
76 uuid: The UUID of the device
77 format: Optional response format parameter
78
79 Returns: The report history of the requested device.
80
81 """
Dirk Vogt62ff7f22017-05-04 16:07:21 +020082 cursor = connection.cursor()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020083 raw_querys.execute_device_report_history(cursor, {"uuid": uuid})
Dirk Vogt62ff7f22017-05-04 16:07:21 +020084 res = dictfetchall(cursor)
85 return Response(res)
86
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020087
Dirk Vogt571168c2017-12-08 16:54:12 +010088class Status(APIView):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020089 """View the number of devices, crashreports and heartbeats."""
90
Borjan Tchakalofffa134bd2018-04-09 16:16:11 +020091 permission_classes = (HasStatsAccess,)
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020092
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020093 def get(self, request, format=None):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +020094 """Get the number of devices, crashreports and heartbeats.
95
96 Args:
97 request: Http request
98 format: Optional response format parameter
99
100 Returns: The number of devices, crashreports and heartbeats.
101
102 """
Dirk Vogt571168c2017-12-08 16:54:12 +0100103 num_devices = Device.objects.count()
104 num_crashreports = Crashreport.objects.count()
105 num_heartbeats = HeartBeat.objects.count()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200106 return Response(
107 {
108 "devices": num_devices,
109 "crashreports": num_crashreports,
110 "heartbeats": num_heartbeats,
111 }
112 )
Dirk Vogt571168c2017-12-08 16:54:12 +0100113
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200114
Dirk Vogt62ff7f22017-05-04 16:07:21 +0200115class DeviceStat(APIView):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200116 """View an overview of the statistics of a device."""
117
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200118 permission_classes = (HasRightsOrIsDeviceOwnerDeviceCreation,)
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200119
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200120 def get(self, request, uuid, format=None):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200121 """Get some general statistics for a device.
122
123 Args:
124 request: Http request
125 uuid: The UUID of the device
126 format: Optional response format parameter
127
128 Returns: Some general information of the device in a dictionary.
129
130 """
131 device = Device.objects.filter(uuid=uuid)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200132 last_active = (
133 HeartBeat.objects.filter(device=device).order_by("-date")[0].date
134 )
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200135 heartbeats = HeartBeat.objects.filter(device=device).count()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200136 crashreports = (
137 Crashreport.objects.filter(device=device)
138 .filter(boot_reason__in=Crashreport.CRASH_BOOT_REASONS)
139 .count()
140 )
141 crashes_per_day = (
142 crashreports * 1.0 / heartbeats if heartbeats > 0 else 0
143 )
144 smpls = (
145 Crashreport.objects.filter(device=device)
146 .filter(boot_reason__in=Crashreport.SMPL_BOOT_REASONS)
147 .count()
148 )
149 smpl_per_day = smpls * 1.0 / heartbeats if heartbeats > 0 else 0
Dirk Vogt62ff7f22017-05-04 16:07:21 +0200150 return Response(
151 {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200152 "uuid": uuid,
153 "last_active": last_active,
154 "heartbeats": heartbeats,
155 "crashreports": crashreports,
156 "crashes_per_day": crashes_per_day,
157 "smpls": smpls,
158 "smpl_per_day": smpl_per_day,
159 "board_date": device[0].board_date,
160 }
161 )
Dirk Vogt62ff7f22017-05-04 16:07:21 +0200162
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200163
Dirk Vogt62ff7f22017-05-04 16:07:21 +0200164class LogFileDownload(APIView):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200165 """View for downloading log files."""
166
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200167 permission_classes = (HasRightsOrIsDeviceOwnerDeviceCreation,)
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200168
169 def get(self, request, id_logfile, format=None):
170 """Get a logfile.
171
172 Args:
173 request: Http request
174 id_logfile: The id of the log file
175 format: Optional response format parameter
176
177 Returns: The log file with the corresponding id.
178
179 """
Dirk Vogt62ff7f22017-05-04 16:07:21 +0200180 try:
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200181 logfile = LogFile.objects.get(id=id_logfile)
182 except ObjectDoesNotExist:
Dirk Vogt62ff7f22017-05-04 16:07:21 +0200183 raise NotFound(detail="Logfile does not exist.")
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200184 zip_file = zipfile.ZipFile(logfile.logfile.path)
Dirk Vogt62ff7f22017-05-04 16:07:21 +0200185 ret = {}
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200186 for file in zip_file.filelist:
187 file_open = zip_file.open(file)
188 ret[file.filename] = file_open.read()
Dirk Vogt62ff7f22017-05-04 16:07:21 +0200189 return Response(ret)
Dirk Vogt1accb672017-05-10 14:07:42 +0200190
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400191
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200192class _VersionStatsFilter(FilterSet):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200193 first_seen_before = DateFilter(
194 field_name="first_seen_on", lookup_expr="lte"
195 )
196 first_seen_after = DateFilter(field_name="first_seen_on", lookup_expr="gte")
197 released_before = DateFilter(field_name="released_on", lookup_expr="lte")
198 released_after = DateFilter(field_name="released_on", lookup_expr="gte")
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200199
Dirk Vogt1accb672017-05-10 14:07:42 +0200200
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400201class _VersionStatsSerializer(serializers.ModelSerializer):
Borjan Tchakalofffa134bd2018-04-09 16:16:11 +0200202 permission_classes = (HasStatsAccess,)
Dirk Vogt1accb672017-05-10 14:07:42 +0200203
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200204
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400205class _VersionStatsListView(generics.ListAPIView):
Borjan Tchakalofffa134bd2018-04-09 16:16:11 +0200206 permission_classes = (HasStatsAccess,)
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200207 filter_backends = (DjangoFilterBackend,)
Dirk Vogt1accb672017-05-10 14:07:42 +0200208
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200209
210class _DailyVersionStatsFilter(FilterSet):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200211 date_start = DateFilter(field_name="date", lookup_expr="gte")
212 date_end = DateFilter(field_name="date", lookup_expr="lte")
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200213
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400214
215class _DailyVersionStatsSerializer(serializers.ModelSerializer):
Borjan Tchakalofffa134bd2018-04-09 16:16:11 +0200216 permission_classes = (HasStatsAccess,)
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400217
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200218
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400219class _DailyVersionStatsListView(generics.ListAPIView):
Borjan Tchakalofffa134bd2018-04-09 16:16:11 +0200220 permission_classes = (HasStatsAccess,)
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200221 filter_backends = (DjangoFilterBackend,)
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400222
223
224class VersionSerializer(_VersionStatsSerializer):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200225 """Serializer for the Version class."""
226
227 class Meta: # noqa: D106
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400228 model = Version
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200229 fields = "__all__"
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400230
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200231
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400232class VersionFilter(_VersionStatsFilter):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200233 """Filter for Version instances."""
234
235 class Meta: # noqa: D106
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400236 model = Version
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200237 fields = "__all__"
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400238
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200239
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400240class VersionListView(_VersionStatsListView):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200241 """View for listing versions."""
242
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200243 queryset = Version.objects.all().order_by("-heartbeats")
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400244 filter_class = VersionFilter
245 serializer_class = VersionSerializer
246
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400247
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200248class VersionDailyFilter(_DailyVersionStatsFilter):
249 """Filter for VersionDaily instances."""
250
251 version__build_fingerprint = CharFilter()
252 version__is_official_release = BooleanFilter()
253 version__is_beta_release = BooleanFilter()
254
255 class Meta: # noqa: D106
Dirk Vogt1accb672017-05-10 14:07:42 +0200256 model = VersionDaily
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200257 fields = "__all__"
Dirk Vogt1accb672017-05-10 14:07:42 +0200258
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200259
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400260class VersionDailySerializer(_DailyVersionStatsSerializer):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200261 """Serializer for VersionDaily instances."""
262
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400263 build_fingerprint = serializers.CharField()
264
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200265 class Meta: # noqa: D106
Dirk Vogt1accb672017-05-10 14:07:42 +0200266 model = VersionDaily
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200267 fields = "__all__"
Dirk Vogt1accb672017-05-10 14:07:42 +0200268
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200269
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400270class VersionDailyListView(_DailyVersionStatsListView):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200271 """View for listing VersionDaily instances."""
272
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400273 queryset = (
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200274 VersionDaily.objects.annotate(
275 build_fingerprint=F("version__build_fingerprint")
276 )
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200277 .all()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200278 .order_by("date")
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400279 )
280 filter_class = VersionDailyFilter
281 filter_fields = (
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200282 "version__build_fingerprint",
283 "version__is_official_release",
284 "version__is_beta_release",
Borjan Tchakaloff227b4312018-02-23 17:02:16 +0400285 )
Dirk Vogt1accb672017-05-10 14:07:42 +0200286 serializer_class = VersionDailySerializer
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400287
288
289class RadioVersionSerializer(_VersionStatsSerializer):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200290 """Serializer for RadioVersion instances."""
291
292 class Meta: # noqa: D106
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400293 model = RadioVersion
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200294 fields = "__all__"
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400295
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200296
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400297class RadioVersionFilter(_VersionStatsFilter):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200298 """Filter for RadioVersion instances."""
299
300 class Meta: # noqa: D106
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400301 model = RadioVersion
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200302 fields = "__all__"
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400303
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200304
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400305class RadioVersionListView(_VersionStatsListView):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200306 """View for listing RadioVersion instances."""
307
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200308 queryset = RadioVersion.objects.all().order_by("-heartbeats")
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400309 serializer_class = RadioVersionSerializer
310 filter_class = RadioVersionFilter
311
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400312
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200313class RadioVersionDailyFilter(_DailyVersionStatsFilter):
314 """Filter for RadioVersionDaily instances."""
315
316 version__radio_version = CharFilter()
317 version__is_official_release = BooleanFilter()
318 version__is_beta_release = BooleanFilter()
319
320 class Meta: # noqa: D106
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400321 model = RadioVersionDaily
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200322 fields = "__all__"
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400323
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200324
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400325class RadioVersionDailySerializer(_DailyVersionStatsSerializer):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200326 """Serializer for RadioVersionDaily instances."""
327
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400328 radio_version = serializers.CharField()
329
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200330 class Meta: # noqa: D106
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400331 model = RadioVersionDaily
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200332 fields = "__all__"
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400333
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200334
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400335class RadioVersionDailyListView(_DailyVersionStatsListView):
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200336 """View for listing RadioVersionDaily instances."""
337
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400338 queryset = (
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200339 RadioVersionDaily.objects.annotate(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200340 radio_version=F("version__radio_version")
341 )
Mitja Nikolaus5c3e0572018-07-30 13:36:14 +0200342 .all()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200343 .order_by("date")
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400344 )
345 filter_class = RadioVersionDailyFilter
346 filter_fields = (
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200347 "version__radio_version",
348 "version__is_official_release",
349 "version__is_beta_release",
Borjan Tchakaloff1db45c72018-02-23 17:03:49 +0400350 )
351 serializer_class = RadioVersionDailySerializer