|
1
|
|
|
# coding: utf8 |
|
2
|
|
|
|
|
3
|
|
|
""" |
|
4
|
|
|
This software is licensed under the Apache 2 license, quoted below. |
|
5
|
|
|
|
|
6
|
|
|
Copyright 2014 Crystalnix Limited |
|
7
|
|
|
|
|
8
|
|
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not |
|
9
|
|
|
use this file except in compliance with the License. You may obtain a copy of |
|
10
|
|
|
the License at |
|
11
|
|
|
|
|
12
|
|
|
http://www.apache.org/licenses/LICENSE-2.0 |
|
13
|
|
|
|
|
14
|
|
|
Unless required by applicable law or agreed to in writing, software |
|
15
|
|
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
|
16
|
|
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
|
17
|
|
|
License for the specific language governing permissions and limitations under |
|
18
|
|
|
the License. |
|
19
|
|
|
""" |
|
20
|
|
|
|
|
21
|
|
|
from django.contrib import admin |
|
22
|
|
|
from django.core.exceptions import ObjectDoesNotExist |
|
23
|
|
|
from django.conf import settings |
|
24
|
|
|
from django.utils.html import format_html |
|
25
|
|
|
|
|
26
|
|
|
from celery import signature |
|
27
|
|
|
|
|
28
|
|
|
from crash.forms import SymbolsAdminForm, CrashFrom |
|
29
|
|
|
from crash.models import Crash, CrashDescription, Symbols |
|
30
|
|
|
from crash.forms import TextInputForm |
|
31
|
|
|
|
|
32
|
|
|
SENTRY_DOMAIN = getattr(settings, 'SENTRY_STACKTRACE_DOMAIN', None) |
|
33
|
|
|
SENTRY_ORG_SLUG = getattr(settings, 'SENTRY_STACKTRACE_ORG_SLUG', None) |
|
34
|
|
|
SENTRY_PROJ_SLUG = getattr(settings, 'SENTRY_STACKTRACE_PROJ_SLUG', None) |
|
35
|
|
|
CRASH_TRACKER = getattr(settings, 'CRASH_TRACKER', None) |
|
36
|
|
|
|
|
37
|
|
|
class BooleanFilter(admin.SimpleListFilter): |
|
38
|
|
|
title = None |
|
39
|
|
|
parameter_name = None |
|
40
|
|
|
|
|
41
|
|
|
def lookups(self, request, model_admin): |
|
42
|
|
|
return ( |
|
43
|
|
|
('yes', 'Yes'), |
|
44
|
|
|
('no', 'No'), |
|
45
|
|
|
) |
|
46
|
|
|
|
|
47
|
|
|
def queryset(self, request, queryset): |
|
48
|
|
|
d = {self.parameter_name: ''} |
|
49
|
|
|
if self.value() == 'yes': |
|
50
|
|
|
return queryset.exclude(**d) |
|
51
|
|
|
if self.value() == 'no': |
|
52
|
|
|
return queryset.filter(**d) |
|
53
|
|
|
|
|
54
|
|
|
|
|
55
|
|
|
class TextInputFilter(admin.filters.FieldListFilter): |
|
56
|
|
|
template = 'admin/textinput_filter.html' |
|
57
|
|
|
|
|
58
|
|
|
def __init__(self, field, request, params, model, model_admin, field_path): |
|
59
|
|
|
self.lookup_kwarg_equal = '%s' % field_path |
|
60
|
|
|
super(TextInputFilter, self).__init__( |
|
61
|
|
|
field, request, params, model, model_admin, field_path) |
|
62
|
|
|
self.form = self.get_form() |
|
63
|
|
|
|
|
64
|
|
|
def choices(self, cl): |
|
65
|
|
|
return [] |
|
66
|
|
|
|
|
67
|
|
|
def expected_parameters(self): |
|
68
|
|
|
return [self.lookup_kwarg_equal] |
|
69
|
|
|
|
|
70
|
|
|
def get_form(self): |
|
71
|
|
|
return TextInputForm(data=self.used_parameters, |
|
72
|
|
|
field_name=self.field_path) |
|
73
|
|
|
|
|
74
|
|
|
def queryset(self, request, queryset): |
|
75
|
|
|
if self.form.is_valid(): |
|
76
|
|
|
filter_params = dict(filter(lambda x: bool(x[1]), |
|
77
|
|
|
self.form.cleaned_data.items())) |
|
78
|
|
|
return queryset.filter(**filter_params) |
|
79
|
|
|
else: |
|
80
|
|
|
return queryset |
|
81
|
|
|
|
|
82
|
|
|
|
|
83
|
|
|
class CrashArchiveFilter(BooleanFilter): |
|
84
|
|
|
title = 'Instrumental file' |
|
85
|
|
|
parameter_name = 'archive' |
|
86
|
|
|
|
|
87
|
|
|
|
|
88
|
|
|
@admin.register(CrashDescription) |
|
89
|
|
|
class CrashDescriptionAdmin(admin.ModelAdmin): |
|
90
|
|
|
readonly_fields = ('created', 'modified') |
|
91
|
|
|
list_display = ('created', 'modified', 'summary') |
|
92
|
|
|
list_display_links = ('created', 'modified', 'summary') |
|
93
|
|
|
|
|
94
|
|
|
|
|
95
|
|
|
class CrashDescriptionInline(admin.StackedInline): |
|
96
|
|
|
model = CrashDescription |
|
97
|
|
|
|
|
98
|
|
|
|
|
99
|
|
|
@admin.register(Crash) |
|
100
|
|
|
class CrashAdmin(admin.ModelAdmin): |
|
101
|
|
|
list_display = ('id', 'created', 'modified', 'archive_field', 'signature', 'appid', 'userid', 'summary_field', 'os', 'build_number', 'channel', 'cpu_architecture_field',) |
|
102
|
|
|
list_select_related = ['crash_description'] |
|
103
|
|
|
list_display_links = ('id', 'created', 'modified', 'signature', 'appid', 'userid', 'cpu_architecture_field',) |
|
104
|
|
|
list_filter = (('id', TextInputFilter,), 'created', CrashArchiveFilter, 'os', 'build_number', 'channel') |
|
105
|
|
|
search_fields = ('appid', 'userid', 'archive',) |
|
106
|
|
|
form = CrashFrom |
|
107
|
|
|
readonly_fields = ['sentry_link_field', 'os', 'build_number', 'channel',] |
|
108
|
|
|
exclude = ('groupid', 'eventid', ) |
|
109
|
|
|
actions = ('regenerate_stacktrace',) |
|
110
|
|
|
inlines = [CrashDescriptionInline] |
|
111
|
|
|
|
|
112
|
|
|
def archive_field(self, obj): |
|
113
|
|
|
return bool(obj.archive) |
|
114
|
|
|
archive_field.short_description = 'Instrumental file' |
|
115
|
|
|
|
|
116
|
|
|
def cpu_architecture_field(self, obj): |
|
117
|
|
|
return obj.stacktrace_json.get('system_info', {}).get('cpu_arch', '') if obj.stacktrace_json else '' |
|
118
|
|
|
cpu_architecture_field.short_description = "CPU Architecture" |
|
119
|
|
|
|
|
120
|
|
|
def sentry_link_field(self, obj): |
|
121
|
|
|
if not SENTRY_DOMAIN or not SENTRY_ORG_SLUG or not SENTRY_PROJ_SLUG: |
|
122
|
|
|
return "Sentry variables are not set" |
|
123
|
|
|
if not obj.groupid or not obj.eventid: |
|
124
|
|
|
return "No sentry link" |
|
125
|
|
|
link = "http://{}/{}/{}/group/{}/events/{}/".format( |
|
126
|
|
|
SENTRY_DOMAIN, |
|
127
|
|
|
SENTRY_ORG_SLUG, |
|
128
|
|
|
SENTRY_PROJ_SLUG, |
|
129
|
|
|
obj.groupid, |
|
130
|
|
|
obj.eventid |
|
131
|
|
|
) |
|
132
|
|
|
return format_html("<a href='{link}'>{link}</a>".format(link=link)) |
|
133
|
|
|
|
|
134
|
|
|
sentry_link_field.short_description = "Sentry link" |
|
135
|
|
|
sentry_link_field.allow_tags = True |
|
136
|
|
|
|
|
137
|
|
|
def summary_field(self, obj): |
|
138
|
|
|
try: |
|
139
|
|
|
return obj.crash_description.summary |
|
140
|
|
|
except ObjectDoesNotExist: |
|
141
|
|
|
return None |
|
142
|
|
|
summary_field.short_description = 'Summary' |
|
143
|
|
|
|
|
144
|
|
|
def regenerate_stacktrace(self, request, queryset): |
|
145
|
|
|
for i in queryset: |
|
146
|
|
|
signature("tasks.processing_crash_dump", args=(i.pk,)).apply_async(queue='default') |
|
147
|
|
|
regenerate_stacktrace.short_description = 'Regenerate stacktrace' |
|
148
|
|
|
|
|
149
|
|
|
def get_form(self, request, obj=None, **kwargs): |
|
150
|
|
|
if CRASH_TRACKER != 'Sentry': |
|
151
|
|
|
try: |
|
152
|
|
|
self.readonly_fields.remove('sentry_link_field') |
|
153
|
|
|
except ValueError: |
|
154
|
|
|
pass |
|
155
|
|
|
return super(CrashAdmin, self).get_form(request, obj, **kwargs) |
|
156
|
|
|
|
|
157
|
|
|
|
|
158
|
|
|
@admin.register(Symbols) |
|
159
|
|
|
class SymbolsAdmin(admin.ModelAdmin): |
|
160
|
|
|
readonly_fields = ('created', 'modified', ) |
|
161
|
|
|
list_display = ('created', 'modified', 'debug_file', 'debug_id',) |
|
162
|
|
|
list_display_links = ('created', 'modified', 'debug_file', 'debug_id',) |
|
163
|
|
|
form = SymbolsAdminForm |
|
164
|
|
|
|
|
165
|
|
|
|