Merge lp:~canonical-isd-hackers/canonical-identity-provider/better-admin into lp:canonical-identity-provider/release

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
Reviewer Review Type Date Requested Status
Canonical ISD hackers Pending
Review via email: mp+23940@code.launchpad.net
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]