/src/sentry/admin.py
Python | 344 lines | 287 code | 53 blank | 4 comment | 5 complexity | 504d753ba6e9c2f38dec6fde59f4c5c2 MD5 | raw file
1from __future__ import absolute_import
2
3from django import forms
4from django.conf import settings
5from django.conf.urls import url
6from django.contrib import admin, messages
7from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AdminPasswordChangeForm
8from django.core.exceptions import PermissionDenied
9from django.db import transaction
10from django.http import Http404, HttpResponseRedirect
11from django.utils.decorators import method_decorator
12from django.views.decorators.csrf import csrf_protect
13from django.views.decorators.debug import sensitive_post_parameters
14from django.shortcuts import get_object_or_404
15from django.template.response import TemplateResponse
16from django.utils.translation import ugettext, ugettext_lazy as _
17from pprint import saferepr
18from sentry.models import (
19 ApiKey,
20 AuthIdentity,
21 AuthProvider,
22 AuditLogEntry,
23 Option,
24 Organization,
25 OrganizationMember,
26 Project,
27 Team,
28 User,
29)
30from sentry.utils.html import escape
31
32csrf_protect_m = method_decorator(csrf_protect)
33sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())
34
35
36class OptionAdmin(admin.ModelAdmin):
37 list_display = ("key", "last_updated")
38 fields = ("key", "value_repr", "last_updated")
39 readonly_fields = ("key", "value_repr", "last_updated")
40 search_fields = ("key",)
41
42 def value_repr(self, instance):
43 return u'<pre style="display:inline-block;white-space:pre-wrap;">{}</pre>'.format(
44 escape(saferepr(instance.value))
45 )
46
47 value_repr.short_description = "Value"
48 value_repr.allow_tags = True
49
50
51admin.site.register(Option, OptionAdmin)
52
53
54class ProjectAdmin(admin.ModelAdmin):
55 list_display = ("name", "slug", "organization", "status", "date_added")
56 list_filter = ("status", "public")
57 search_fields = ("name", "organization__slug", "organization__name", "slug")
58 raw_id_fields = ("organization",)
59 readonly_fields = ("first_event", "date_added")
60
61
62admin.site.register(Project, ProjectAdmin)
63
64
65class OrganizationApiKeyInline(admin.TabularInline):
66 model = ApiKey
67 extra = 1
68 fields = ("label", "key", "status", "allowed_origins", "date_added")
69 raw_id_fields = ("organization",)
70
71
72class OrganizationProjectInline(admin.TabularInline):
73 model = Project
74 extra = 1
75 fields = ("name", "slug", "status", "date_added")
76 raw_id_fields = ("organization",)
77
78
79class OrganizationTeamInline(admin.TabularInline):
80 model = Team
81 extra = 1
82 fields = ("name", "slug", "status", "date_added")
83 raw_id_fields = ("organization",)
84
85
86class OrganizationMemberInline(admin.TabularInline):
87 model = OrganizationMember
88 extra = 1
89 fields = ("user", "organization", "role")
90 raw_id_fields = ("user", "organization")
91
92
93class OrganizationUserInline(OrganizationMemberInline):
94 fk_name = "user"
95
96
97class AuthIdentityInline(admin.TabularInline):
98 model = AuthIdentity
99 extra = 1
100 fields = ("user", "auth_provider", "ident", "data", "last_verified")
101 raw_id_fields = ("user", "auth_provider")
102
103
104class OrganizationAdmin(admin.ModelAdmin):
105 list_display = ("name", "slug", "status")
106 list_filter = ("status",)
107 search_fields = ("name", "slug")
108 fields = ("name", "slug", "status")
109 inlines = (
110 OrganizationMemberInline,
111 OrganizationTeamInline,
112 OrganizationProjectInline,
113 OrganizationApiKeyInline,
114 )
115
116
117admin.site.register(Organization, OrganizationAdmin)
118
119
120class AuthProviderAdmin(admin.ModelAdmin):
121 list_display = ("organization", "provider", "date_added")
122 search_fields = ("organization__name",)
123 raw_id_fields = ("organization", "default_teams")
124 list_filter = ("provider",)
125
126
127admin.site.register(AuthProvider, AuthProviderAdmin)
128
129
130class AuthIdentityAdmin(admin.ModelAdmin):
131 list_display = ("user", "auth_provider", "ident", "date_added", "last_verified")
132 list_filter = ("auth_provider__provider",)
133 search_fields = ("user__email", "user__username", "auth_provider__organization__name")
134 raw_id_fields = ("user", "auth_provider")
135
136
137admin.site.register(AuthIdentity, AuthIdentityAdmin)
138
139
140class TeamAdmin(admin.ModelAdmin):
141 list_display = ("name", "slug", "organization", "status", "date_added")
142 list_filter = ("status",)
143 search_fields = ("name", "organization__name", "slug")
144 raw_id_fields = ("organization",)
145
146 def save_model(self, request, obj, form, change):
147 prev_org = obj.organization_id
148 super(TeamAdmin, self).save_model(request, obj, form, change)
149 if not change:
150 return
151 new_org = obj.organization_id
152 if new_org != prev_org:
153 return
154
155 obj.transfer_to(obj.organization)
156
157
158admin.site.register(Team, TeamAdmin)
159
160
161class UserChangeForm(UserChangeForm):
162 username = forms.RegexField(
163 label=_("Username"),
164 max_length=128,
165 regex=r"^[\w.@+-]+$",
166 help_text=_("Required. 128 characters or fewer. Letters, digits and " "@/./+/-/_ only."),
167 error_messages={
168 "invalid": _(
169 "This value may contain only letters, numbers and " "@/./+/-/_ characters."
170 )
171 },
172 )
173
174
175class UserCreationForm(UserCreationForm):
176 username = forms.RegexField(
177 label=_("Username"),
178 max_length=128,
179 regex=r"^[\w.@+-]+$",
180 help_text=_("Required. 128 characters or fewer. Letters, digits and " "@/./+/-/_ only."),
181 error_messages={
182 "invalid": _(
183 "This value may contain only letters, numbers and " "@/./+/-/_ characters."
184 )
185 },
186 )
187
188
189class UserAdmin(admin.ModelAdmin):
190 add_form_template = "admin/auth/user/add_form.html"
191 change_user_password_template = None
192 fieldsets = (
193 (None, {"fields": ("username", "password")}),
194 (_("Personal info"), {"fields": ("name", "email")}),
195 (_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser")}),
196 (_("Important dates"), {"fields": ("last_login", "date_joined")}),
197 )
198 add_fieldsets = (
199 (None, {"classes": ("wide",), "fields": ("username", "password1", "password2")}),
200 )
201 form = UserChangeForm
202 add_form = UserCreationForm
203 change_password_form = AdminPasswordChangeForm
204 list_display = ("username", "email", "name", "is_staff", "date_joined")
205 list_filter = ("is_staff", "is_superuser", "is_active", "is_managed")
206 search_fields = ("username", "name", "email")
207 ordering = ("username",)
208 inlines = (OrganizationUserInline, AuthIdentityInline)
209
210 def get_fieldsets(self, request, obj=None):
211 if not obj:
212 return self.add_fieldsets
213 return super(UserAdmin, self).get_fieldsets(request, obj)
214
215 def get_form(self, request, obj=None, **kwargs):
216 """
217 Use special form during user creation
218 """
219 defaults = {}
220 if obj is None:
221 defaults.update(
222 {"form": self.add_form, "fields": admin.util.flatten_fieldsets(self.add_fieldsets)}
223 )
224 defaults.update(kwargs)
225 return super(UserAdmin, self).get_form(request, obj, **defaults)
226
227 def get_urls(self):
228 return [
229 url(r"^(\d+)/password/$", self.admin_site.admin_view(self.user_change_password))
230 ] + super(UserAdmin, self).get_urls()
231
232 def lookup_allowed(self, lookup, value):
233 # See #20078: we don't want to allow any lookups involving passwords.
234 if lookup.startswith("password"):
235 return False
236 return super(UserAdmin, self).lookup_allowed(lookup, value)
237
238 @sensitive_post_parameters_m
239 @csrf_protect_m
240 @transaction.atomic
241 def add_view(self, request, form_url="", extra_context=None):
242 # It's an error for a user to have add permission but NOT change
243 # permission for users. If we allowed such users to add users, they
244 # could create superusers, which would mean they would essentially have
245 # the permission to change users. To avoid the problem entirely, we
246 # disallow users from adding users if they don't have change
247 # permission.
248 if not self.has_change_permission(request):
249 if self.has_add_permission(request) and settings.DEBUG:
250 # Raise Http404 in debug mode so that the user gets a helpful
251 # error message.
252 raise Http404(
253 'Your user does not have the "Change user" permission. In '
254 "order to add users, Django requires that your user "
255 'account have both the "Add user" and "Change user" '
256 "permissions set."
257 )
258 raise PermissionDenied
259 if extra_context is None:
260 extra_context = {}
261 username_field = self.model._meta.get_field(self.model.USERNAME_FIELD)
262 defaults = {"auto_populated_fields": (), "username_help_text": username_field.help_text}
263 extra_context.update(defaults)
264 return super(UserAdmin, self).add_view(request, form_url, extra_context)
265
266 @sensitive_post_parameters_m
267 def user_change_password(self, request, id, form_url=""):
268 if not self.has_change_permission(request):
269 raise PermissionDenied
270 user = get_object_or_404(self.queryset(request), pk=id)
271 if request.method == "POST":
272 form = self.change_password_form(user, request.POST)
273 if form.is_valid():
274 form.save()
275 msg = ugettext("Password changed successfully.")
276 messages.success(request, msg)
277 return HttpResponseRedirect("..")
278 else:
279 form = self.change_password_form(user)
280
281 fieldsets = [(None, {"fields": list(form.base_fields)})]
282 adminForm = admin.helpers.AdminForm(form, fieldsets, {})
283
284 context = {
285 "title": _("Change password: %s") % escape(user.get_username()),
286 "adminForm": adminForm,
287 "form_url": form_url,
288 "form": form,
289 "is_popup": "_popup" in request.GET,
290 "add": True,
291 "change": False,
292 "has_delete_permission": False,
293 "has_change_permission": True,
294 "has_absolute_url": False,
295 "opts": self.model._meta,
296 "original": user,
297 "save_as": False,
298 "show_save": True,
299 }
300 return TemplateResponse(
301 request,
302 self.change_user_password_template or "admin/auth/user/change_password.html",
303 context,
304 current_app=self.admin_site.name,
305 )
306
307 def response_add(self, request, obj, post_url_continue=None):
308 """
309 Determines the HttpResponse for the add_view stage. It mostly defers to
310 its superclass implementation but is customized because the User model
311 has a slightly different workflow.
312 """
313 # We should allow further modification of the user just added i.e. the
314 # 'Save' button should behave like the 'Save and continue editing'
315 # button except in two scenarios:
316 # * The user has pressed the 'Save and add another' button
317 # * We are adding a user in a popup
318 if "_addanother" not in request.POST and "_popup" not in request.POST:
319 request.POST["_continue"] = 1
320 return super(UserAdmin, self).response_add(request, obj, post_url_continue)
321
322
323admin.site.register(User, UserAdmin)
324
325
326class AuditLogEntryAdmin(admin.ModelAdmin):
327 list_display = ("event", "organization", "actor", "datetime")
328 list_filter = ("event", "datetime")
329 search_fields = ("actor__email", "organization__name", "organization__slug")
330 raw_id_fields = ("organization", "actor", "target_user")
331 readonly_fields = (
332 "organization",
333 "actor",
334 "actor_key",
335 "target_object",
336 "target_user",
337 "event",
338 "ip_address",
339 "data",
340 "datetime",
341 )
342
343
344admin.site.register(AuditLogEntry, AuditLogEntryAdmin)