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
|
|
|
|