Merge lp:~canonical-isd-hackers/canonical-identity-provider/better-admin into lp:canonical-identity-provider/release
- better-admin
- Merge into trunk
Proposed by
Łukasz Czyżykowski
Status: | Superseded |
---|---|
Proposed branch: | lp:~canonical-isd-hackers/canonical-identity-provider/better-admin |
Merge into: | lp:canonical-identity-provider/release |
Diff against target: |
415 lines (+310/-6) 6 files modified
identityprovider/admin.py (+197/-3) identityprovider/models/account.py (+3/-1) identityprovider/models/api.py (+1/-1) identityprovider/models/const.py (+17/-0) identityprovider/models/emailaddress.py (+1/-1) scripts/generate-random-accounts (+91/-0) |
To merge this branch: | bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/better-admin |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Canonical ISD hackers | Pending | ||
Review via email: mp+23940@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'identityprovider/admin.py' |
2 | --- identityprovider/admin.py 2010-04-21 15:29:24 +0000 |
3 | +++ identityprovider/admin.py 2010-04-30 13:24:33 +0000 |
4 | @@ -3,22 +3,38 @@ |
5 | |
6 | from django.contrib import admin |
7 | from django import forms |
8 | +from django.utils.safestring import mark_safe |
9 | +from django.utils.translation import ugettext_lazy as _ |
10 | |
11 | from identityprovider.models import (Account, AccountPassword, EmailAddress, |
12 | OpenIDRPConfig, APIUser) |
13 | +from identityprovider.utils import encrypt_launchpad_password |
14 | from identityprovider.const import SREG_LABELS |
15 | +from identityprovider.models.const import AccountStatus, EmailStatus |
16 | from identityprovider.fields import CommaSeparatedField |
17 | |
18 | |
19 | class OpenIDRPConfigForm(forms.ModelForm): |
20 | - displayname = forms.CharField() |
21 | + displayname = forms.CharField(label=_("Display name")) |
22 | trust_root = forms.URLField(verify_exists=False) |
23 | logo = forms.CharField(required=False) |
24 | allowed_sreg = CommaSeparatedField( |
25 | - choices=SREG_LABELS.items(), required=False) |
26 | + choices=SREG_LABELS.items(), |
27 | + required=False, |
28 | + widget=forms.CheckboxSelectMultiple |
29 | + ) |
30 | + can_query_any_team = forms.BooleanField(label=_("Can query private teams"), |
31 | + required=False) |
32 | + auto_authorize = forms.BooleanField(label=_("Auto-authorize"), |
33 | + required=False) |
34 | |
35 | |
36 | class OpenIDRPConfigAdmin(admin.ModelAdmin): |
37 | + class Media: |
38 | + css = { |
39 | + 'all': ("/assets/identityprovider/admin-fixes.css",) |
40 | + } |
41 | + |
42 | form = OpenIDRPConfigForm |
43 | list_display = ('displayname', 'trust_root', 'can_query_any_team', |
44 | 'auto_authorize') |
45 | @@ -27,17 +43,195 @@ |
46 | search_fields = ('displayname', 'trust_root') |
47 | |
48 | |
49 | +class AccountPasswordInlineForm(forms.ModelForm): |
50 | + class Meta: |
51 | + model = AccountPassword |
52 | + |
53 | + password = forms.CharField(widget=forms.PasswordInput, required=False, |
54 | + label="") |
55 | + |
56 | + def __init__(self, *args, **kwargs): |
57 | + initial = kwargs.get('initial', {}) |
58 | + initial.update({'password': ''}) |
59 | + kwargs['initial'] = initial |
60 | + super(AccountPasswordInlineForm, self).__init__(*args, **kwargs) |
61 | + |
62 | + |
63 | +class AccountPasswordFormset(forms.models.BaseInlineFormSet): |
64 | + def __init__(self, *args, **kwargs): |
65 | + super(AccountPasswordFormset, self).__init__(*args, **kwargs) |
66 | + self.can_delete = False |
67 | + |
68 | + |
69 | class AccountPasswordInline(admin.TabularInline): |
70 | model = AccountPassword |
71 | + form = AccountPasswordInlineForm |
72 | + formset = AccountPasswordFormset |
73 | + |
74 | + |
75 | +class EmailAddressInlineForm(forms.ModelForm): |
76 | + class Meta: |
77 | + model = EmailAddress |
78 | + exclude = ('person',) |
79 | + |
80 | + email = forms.CharField(widget=forms.TextInput(attrs={'size': 40}), |
81 | + label=_("Email address")) |
82 | + |
83 | + |
84 | +class EmailAddressInlineFormSet(forms.models.BaseInlineFormSet): |
85 | + |
86 | + def clean(self): |
87 | + super(EmailAddressInlineFormSet, self).clean() |
88 | + count = 0 |
89 | + for form in self.forms: |
90 | + if form.is_valid() and form.cleaned_data: |
91 | + status = form.cleaned_data['status'] |
92 | + if status == EmailStatus.PREFERRED: |
93 | + count += 1 |
94 | + if count > 1: |
95 | + raise forms.ValidationError( |
96 | + _("Only one email address can be preferred.")) |
97 | |
98 | |
99 | class EmailAddressInline(admin.TabularInline): |
100 | model = EmailAddress |
101 | + form = EmailAddressInlineForm |
102 | + formset = EmailAddressInlineFormSet |
103 | + verbose_name_plural = _("Email addresses") |
104 | + |
105 | + |
106 | +class StatusWidget(forms.Select): |
107 | + |
108 | + date_status_set = None |
109 | + |
110 | + def render(self, name, value, attrs=None, choices=()): |
111 | + select = super(StatusWidget, self).render(name, value, attrs, choices) |
112 | + if self.date_status_set: |
113 | + status_date = _("Set on %s") % self.date_status_set |
114 | + else: |
115 | + status_date = "" |
116 | + return mark_safe("%s %s" % (select, status_date)) |
117 | + |
118 | + |
119 | +class ReadOnlyDateTime(forms.widgets.Widget): |
120 | + |
121 | + value = None |
122 | + |
123 | + def render(self, name, value, attrs=None): |
124 | + return str(self.value) |
125 | + |
126 | + |
127 | +class LPUsernameWidget(forms.widgets.Widget): |
128 | + |
129 | + account = None |
130 | + |
131 | + def render(self, name, value, attrs=None): |
132 | + return mark_safe(account_to_lp_link(self.account)) |
133 | + |
134 | + |
135 | +class LPUsernameField(forms.Field): |
136 | + widget = LPUsernameWidget |
137 | + |
138 | + |
139 | +class AccountAdminForm(forms.ModelForm): |
140 | + class Meta: |
141 | + model = Account |
142 | + exclude = ('date_status_set',) |
143 | + |
144 | + def __init__(self, *args, **kwargs): |
145 | + instance = kwargs.get('instance') |
146 | + if instance: |
147 | + widget = self.declared_fields['status'].widget |
148 | + widget.date_status_set = instance.date_status_set |
149 | + |
150 | + widget = self.declared_fields['date_created'].widget |
151 | + widget.value = instance.date_created |
152 | + |
153 | + widget = self.declared_fields['lp_username'].widget |
154 | + widget.account = instance |
155 | + super(AccountAdminForm, self).__init__(*args, **kwargs) |
156 | + |
157 | + date_created = forms.DateTimeField(widget=ReadOnlyDateTime, required=False) |
158 | + lp_username = LPUsernameField(label=_("LP username"), required=False) |
159 | + status = forms.TypedChoiceField(widget=StatusWidget, |
160 | + choices=AccountStatus._get_choices()) |
161 | + |
162 | + displayname = forms.CharField(label=_("Display name"), |
163 | + widget=forms.TextInput) |
164 | + openid_identifier = forms.CharField(label=_("OpenID identifier"), |
165 | + widget=forms.TextInput) |
166 | + |
167 | + |
168 | +def account_to_lp_link(account): |
169 | + if account and account.person: |
170 | + return ('<a href="https://launchpad.net/~%s">%s</a>' % |
171 | + (account.person.name, account.person.name)) |
172 | + return "" |
173 | |
174 | |
175 | class AccountAdmin(admin.ModelAdmin): |
176 | inlines = [AccountPasswordInline, EmailAddressInline] |
177 | |
178 | + form = AccountAdminForm |
179 | + |
180 | + def preferred_email(self, account): |
181 | + return account.preferredemail or '' |
182 | + |
183 | + def name(self, account): |
184 | + return account_to_lp_link(account) |
185 | + name.short_description = _("LP username") |
186 | + name.allow_tags = True |
187 | + |
188 | + def save_formset(self, request, form, formset, change): |
189 | + if formset.model is AccountPassword: |
190 | + password = formset.forms[0].cleaned_data.get('password') |
191 | + instances = formset.save(commit=False) |
192 | + if len(instances) == 1 and password: |
193 | + instance = instances[0] |
194 | + encrypted_password = encrypt_launchpad_password(password) |
195 | + instance.password = encrypted_password |
196 | + instance.save() |
197 | + else: |
198 | + super(AccountAdmin, self).save_formset( |
199 | + request, form, formset, change) |
200 | + |
201 | + fieldsets = ( |
202 | + (None, { |
203 | + 'fields': ('date_created', 'lp_username', 'creation_rationale', |
204 | + 'status', 'displayname', 'openid_identifier') |
205 | + }), |
206 | + ) |
207 | + |
208 | + list_display = ('__unicode__', 'preferred_email', 'name', 'status') |
209 | + search_fields = ('displayname', 'emailaddress__email', 'openid_identifier') |
210 | + list_filter = ('date_created', 'status', 'date_status_set') |
211 | + |
212 | + |
213 | +class APIUserAdminForm(forms.ModelForm): |
214 | + class Meta: |
215 | + model = APIUser |
216 | + |
217 | + def __init__(self, *args, **kwargs): |
218 | + kwargs['initial'] = {'password': ''} |
219 | + super(APIUserAdminForm, self).__init__(*args, **kwargs) |
220 | + |
221 | + password = forms.CharField(required=False) |
222 | + |
223 | + |
224 | +class APIUserAdmin(admin.ModelAdmin): |
225 | + model = APIUser |
226 | + |
227 | + form = APIUserAdminForm |
228 | + |
229 | + search_fields = ['username'] |
230 | + |
231 | + def save_model(self, request, obj, form, change): |
232 | + password = form.cleaned_data.get('password') |
233 | + if password: |
234 | + obj.set_password(password) |
235 | + obj.save() |
236 | + |
237 | + |
238 | admin.site.register(Account, AccountAdmin) |
239 | admin.site.register(OpenIDRPConfig, OpenIDRPConfigAdmin) |
240 | -admin.site.register(APIUser) |
241 | +admin.site.register(APIUser, APIUserAdmin) |
242 | |
243 | === modified file 'identityprovider/models/account.py' |
244 | --- identityprovider/models/account.py 2010-04-21 15:29:24 +0000 |
245 | +++ identityprovider/models/account.py 2010-04-30 13:24:33 +0000 |
246 | @@ -59,10 +59,12 @@ |
247 | class Account(models.Model): |
248 | implements(IAccount) |
249 | |
250 | - date_created = models.DateTimeField(default=datetime.datetime.utcnow) |
251 | + date_created = models.DateTimeField(default=datetime.datetime.utcnow, |
252 | + editable=False) |
253 | creation_rationale = \ |
254 | models.IntegerField( |
255 | choices=AccountCreationRationale._get_choices()) |
256 | + |
257 | status = models.IntegerField(choices=AccountStatus._get_choices()) |
258 | date_status_set = models.DateTimeField(default=datetime.datetime.utcnow) |
259 | displayname = models.TextField() |
260 | |
261 | === modified file 'identityprovider/models/api.py' |
262 | --- identityprovider/models/api.py 2010-04-21 15:29:24 +0000 |
263 | +++ identityprovider/models/api.py 2010-04-30 13:24:33 +0000 |
264 | @@ -10,7 +10,7 @@ |
265 | |
266 | class APIUser(models.Model): |
267 | username = models.CharField(max_length=256) |
268 | - password = models.CharField(max_length=256) |
269 | + password = models.CharField(max_length=256, editable=False) |
270 | |
271 | created_at = models.DateTimeField(auto_now_add=True) |
272 | updated_at = models.DateTimeField(auto_now=True) |
273 | |
274 | === modified file 'identityprovider/models/const.py' |
275 | --- identityprovider/models/const.py 2010-04-21 15:29:24 +0000 |
276 | +++ identityprovider/models/const.py 2010-04-30 13:24:33 +0000 |
277 | @@ -1,5 +1,7 @@ |
278 | # Copyright 2010 Canonical Ltd. This software is licensed under the |
279 | # GNU Affero General Public License version 3 (see the file LICENSE). |
280 | +from django.utils.translation import ugettext_lazy as _ |
281 | + |
282 | |
283 | |
284 | class BaseConst: |
285 | @@ -42,6 +44,21 @@ |
286 | DEACTIVATED = 30 |
287 | SUSPENDED = 40 |
288 | |
289 | + _verbose = { |
290 | + # The account has not yet been activated. |
291 | + NOACCOUNT: _("No activated"), |
292 | + # The account is active. |
293 | + ACTIVE: _("Active"), |
294 | + # The account has been deactivated by the account's owner. |
295 | + DEACTIVATED: _("Deactivated (by user)"), |
296 | + # The account has been suspended by an admin. |
297 | + SUSPENDED: _("Suspended (by admin)"), |
298 | + } |
299 | + |
300 | + @classmethod |
301 | + def _get_choices(cls): |
302 | + return sorted(cls._verbose.items()) |
303 | + |
304 | |
305 | class EmailStatus(BaseConst): |
306 | NEW = 1 |
307 | |
308 | === modified file 'identityprovider/models/emailaddress.py' |
309 | --- identityprovider/models/emailaddress.py 2010-04-21 15:29:24 +0000 |
310 | +++ identityprovider/models/emailaddress.py 2010-04-30 13:24:33 +0000 |
311 | @@ -16,7 +16,7 @@ |
312 | class EmailAddress(models.Model): |
313 | email = models.TextField() |
314 | person = models.ForeignKey(Person, db_column='person', |
315 | - blank=True, null=True) |
316 | + blank=True, null=True, editable=False) |
317 | status = models.IntegerField(choices=EmailStatus._get_choices()) |
318 | date_created = models.DateTimeField(default=datetime.datetime.utcnow, |
319 | blank=True, editable=False) |
320 | |
321 | === added file 'scripts/generate-random-accounts' |
322 | --- scripts/generate-random-accounts 1970-01-01 00:00:00 +0000 |
323 | +++ scripts/generate-random-accounts 2010-04-30 13:24:33 +0000 |
324 | @@ -0,0 +1,91 @@ |
325 | +#!/usr/bin/env python |
326 | +import os |
327 | +import sys |
328 | +import random |
329 | +import string |
330 | +import math |
331 | + |
332 | +os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' |
333 | +sys.path.append('.') |
334 | + |
335 | +# percent of accounts with associated person |
336 | +ACCOUNTS_WITH_PERSON = 0.8 |
337 | +ACCOUNT_MAX_EMAILS = 5 |
338 | + |
339 | +from identityprovider.models import Account, Person, LPAccount |
340 | +from identityprovider.models.const import ( |
341 | + AccountCreationRationale, EmailStatus, AccountStatus) |
342 | + |
343 | +def generate_string(length): |
344 | + return "".join(random.choice(string.lowercase) |
345 | + for x in range(random.randint(length - 3, length + 3))) |
346 | + |
347 | +def generate_password(): |
348 | + return (generate_string(10) + random.choice(string.digits) + |
349 | + random.choice(string.uppercase)) |
350 | + |
351 | +def generate_email(): |
352 | + return "%s@%s.%s" % (generate_string(5), generate_string(8), |
353 | + random.choice(["com", "edu", "co.uk", "pl", "org"])) |
354 | + |
355 | +def generate_lp_name(): |
356 | + while True: |
357 | + name = "-".join(generate_string(6) |
358 | + for c in range(random.randint(1, 3))) |
359 | + if Person.objects.filter(name=name).count() == 0: |
360 | + return name |
361 | + |
362 | +def generate_person(account): |
363 | + Person.objects.create( |
364 | + displayname=account.displayname, |
365 | + name=generate_lp_name(), |
366 | + lp_account=LPAccount.objects.create( |
367 | + openid_identifier=account.openid_identifier) |
368 | + ) |
369 | + |
370 | +def generate_account(): |
371 | + account = Account.objects.create_account( |
372 | + " ".join([generate_string(5).title(), generate_string(5).title()]), |
373 | + generate_email(), |
374 | + generate_password(), |
375 | + random.choice(dict(AccountCreationRationale._get_choices()).keys()) |
376 | + ) |
377 | + for i in range(random.randint(0, ACCOUNT_MAX_EMAILS - 1)): |
378 | + try: |
379 | + account.emailaddress_set.create( |
380 | + qemail=generate_email(), |
381 | + status=random.choice([EmailStatus.NEW, |
382 | + EmailStatus.VALIDATED, |
383 | + EmailStatus.OLD]) |
384 | + ) |
385 | + except Exception: |
386 | + # Ignore case when generated email is already in the db |
387 | + pass |
388 | + if random.random() > (1.0 - ACCOUNTS_WITH_PERSON): |
389 | + generate_person(account) |
390 | + |
391 | + statuses = [(0.9, AccountStatus.SUSPENDED), |
392 | + (0.8, AccountStatus.DEACTIVATED), |
393 | + (0.6, AccountStatus.NOACCOUNT), |
394 | + (0.0, AccountStatus.ACTIVE)] |
395 | + for probability, status in statuses: |
396 | + if random.random() > probability: |
397 | + account.status = status |
398 | + account.save() |
399 | + break |
400 | + return account |
401 | + |
402 | +if __name__ == '__main__': |
403 | + try: |
404 | + accounts_count = int(sys.argv[1]) |
405 | + print "Generating %d accounts" % accounts_count |
406 | + |
407 | + interval = 10 ** (int(math.log10(accounts_count)) - 1) |
408 | + for i in xrange(accounts_count): |
409 | + generate_account() |
410 | + if i > 0 and (i % interval) == 0: |
411 | + print "Created %d accounts" % i |
412 | + print "Created %d accounts" % accounts_count |
413 | + |
414 | + except (ValueError, IndexError, TypeError): |
415 | + print "Usage: %s number-of-accounts-to-create" % sys.argv[0] |