diff options
Diffstat (limited to 'devel/views.py')
-rw-r--r-- | devel/views.py | 270 |
1 files changed, 124 insertions, 146 deletions
diff --git a/devel/views.py b/devel/views.py index 7be3dc17..4258ea7f 100644 --- a/devel/views.py +++ b/devel/views.py @@ -1,42 +1,40 @@ -from datetime import date, datetime, timedelta +from datetime import timedelta import operator import pytz -import random -from string import ascii_letters, digits import time -from django import forms -from django.conf import settings from django.http import HttpResponseRedirect from django.contrib.auth.decorators import \ login_required, permission_required, user_passes_test -from django.contrib.auth.models import User, Group -from django.contrib.sites.models import Site -from django.core.mail import send_mail +from django.contrib.admin.models import LogEntry, ADDITION +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType from django.db import transaction -from django.db.models import F +from django.db.models import F, Count, Max from django.http import Http404 -from django.shortcuts import get_object_or_404 -from django.template import loader, Context +from django.shortcuts import get_object_or_404, render from django.template.defaultfilters import filesizeformat from django.views.decorators.cache import never_cache -from django.views.generic.simple import direct_to_template +from django.utils.encoding import force_unicode from django.utils.http import http_date +from django.utils.timezone import now -from .models import UserProfile -from main.models import Package, PackageDepend, PackageFile, TodolistPkg +from .forms import ProfileForm, UserProfileForm, NewUserForm +from .models import DeveloperKey +from main.models import Package, PackageFile from main.models import Arch, Repo -from main.utils import utc_now -from packages.models import PackageRelation +from news.models import News +from packages.models import PackageRelation, Signoff, FlagRequest, Depend from packages.utils import get_signoff_groups +from todolists.models import TodolistPackage from todolists.utils import get_annotated_todolists -from .utils import get_annotated_maintainers, UserFinder +from .utils import get_annotated_maintainers @login_required def index(request): '''the developer dashboard''' - if(request.user.is_authenticated()): + if request.user.is_authenticated(): inner_q = PackageRelation.objects.filter(user=request.user) else: inner_q = PackageRelation.objects.none() @@ -45,17 +43,28 @@ def index(request): flagged = Package.objects.normal().filter( flag_date__isnull=False, pkgbase__in=inner_q).order_by('pkgname') - todopkgs = TodolistPkg.objects.select_related( - 'pkg', 'pkg__arch', 'pkg__repo').filter(complete=False) - todopkgs = todopkgs.filter(pkg__pkgbase__in=inner_q).order_by( - 'list__name', 'pkg__pkgname') + todopkgs = TodolistPackage.objects.select_related( + 'todolist', 'pkg', 'arch', 'repo').exclude( + status=TodolistPackage.COMPLETE).filter(removed__isnull=True) + todopkgs = todopkgs.filter(pkgbase__in=inner_q).order_by( + 'todolist__name', 'pkgname') - todolists = get_annotated_todolists() - todolists = [todolist for todolist in todolists if todolist.incomplete_count > 0] + todolists = get_annotated_todolists(incomplete_only=True) signoffs = sorted(get_signoff_groups(user=request.user), key=operator.attrgetter('pkgbase')) + arches = Arch.objects.all().annotate( + total_ct=Count('packages'), flagged_ct=Count('packages__flag_date')) + repos = Repo.objects.all().annotate( + total_ct=Count('packages'), flagged_ct=Count('packages__flag_date')) + # the join is huge unless we do this separately, so merge the result here + repo_maintainers = dict(Repo.objects.order_by().filter( + userprofile__user__is_active=True).values_list('id').annotate( + Count('userprofile'))) + for repo in repos: + repo.maintainer_ct = repo_maintainers.get(repo.id, 0) + maintainers = get_annotated_maintainers() maintained = PackageRelation.objects.filter( @@ -72,80 +81,93 @@ def index(request): page_dict = { 'todos': todolists, - 'repos': Repo.objects.all(), - 'arches': Arch.objects.all(), + 'arches': arches, + 'repos': repos, 'maintainers': maintainers, 'orphan': orphan, - 'flagged' : flagged, - 'todopkgs' : todopkgs, + 'flagged': flagged, + 'todopkgs': todopkgs, 'signoffs': signoffs } - return direct_to_template(request, 'devel/index.html', page_dict) + return render(request, 'devel/index.html', page_dict) + @login_required def clock(request): devs = User.objects.filter(is_active=True).order_by( 'first_name', 'last_name').select_related('userprofile') - now = utc_now() + latest_news = dict(News.objects.filter( + author__is_active=True).values_list('author').order_by( + ).annotate(last_post=Max('postdate'))) + latest_package = dict(Package.objects.filter( + packager__is_active=True).values_list('packager').order_by( + ).annotate(last_build=Max('build_date'))) + latest_signoff = dict(Signoff.objects.filter( + user__is_active=True).values_list('user').order_by( + ).annotate(last_signoff=Max('created'))) + # The extra() bit ensures we can use our 'user_id IS NOT NULL' index + latest_flagreq = dict(FlagRequest.objects.filter( + user__is_active=True).extra( + where=['user_id IS NOT NULL']).values_list('user_id').order_by( + ).annotate(last_flagrequest=Max('created'))) + latest_log = dict(LogEntry.objects.filter( + user__is_active=True).values_list('user').order_by( + ).annotate(last_log=Max('action_time'))) + + for dev in devs: + dates = [ + latest_news.get(dev.id, None), + latest_package.get(dev.id, None), + latest_signoff.get(dev.id, None), + latest_flagreq.get(dev.id, None), + latest_log.get(dev.id, None), + dev.last_login, + ] + dates = [d for d in dates if d is not None] + if dates: + dev.last_action = max(dates) + else: + dev.last_action = None + + current_time = now() page_dict = { 'developers': devs, - 'utc_now': now, + 'utc_now': current_time, } - response = direct_to_template(request, 'devel/clock.html', page_dict) + response = render(request, 'devel/clock.html', page_dict) if not response.has_header('Expires'): - expire_time = now.replace(second=0, microsecond=0) + expire_time = current_time.replace(second=0, microsecond=0) expire_time += timedelta(minutes=1) expire_time = time.mktime(expire_time.timetuple()) response['Expires'] = http_date(expire_time) return response -class ProfileForm(forms.Form): - email = forms.EmailField(label='Private email (not shown publicly):', - help_text="Used for out-of-date notifications, etc.") - passwd1 = forms.CharField(label='New Password', required=False, - widget=forms.PasswordInput) - passwd2 = forms.CharField(label='Confirm Password', required=False, - widget=forms.PasswordInput) - - def clean(self): - if self.cleaned_data['passwd1'] != self.cleaned_data['passwd2']: - raise forms.ValidationError('Passwords do not match.') - return self.cleaned_data - -class UserProfileForm(forms.ModelForm): - def clean_pgp_key(self): - data = self.cleaned_data['pgp_key'] - # strip 0x prefix if provided; store uppercase - if data.startswith('0x'): - data = data[2:] - return data.upper() - - class Meta: - model = UserProfile - exclude = ('allowed_repos', 'user', 'latin_name') @login_required @never_cache def change_profile(request): if request.POST: form = ProfileForm(request.POST) - profile_form = UserProfileForm(request.POST, request.FILES, instance=request.user.get_profile()) + profile_form = UserProfileForm(request.POST, request.FILES, + instance=request.user.userprofile) if form.is_valid() and profile_form.is_valid(): request.user.email = form.cleaned_data['email'] if form.cleaned_data['passwd1']: request.user.set_password(form.cleaned_data['passwd1']) - request.user.save() - profile_form.save() + with transaction.commit_on_success(): + request.user.save() + profile_form.save() return HttpResponseRedirect('/devel/') else: form = ProfileForm(initial={'email': request.user.email}) - profile_form = UserProfileForm(instance=request.user.get_profile()) - return direct_to_template(request, 'devel/profile.html', + profile_form = UserProfileForm(instance=request.user.userprofile) + return render(request, 'devel/profile.html', {'form': form, 'profile_form': profile_form}) + @login_required def report(request, report_name, username=None): title = 'Developer Report' @@ -163,12 +185,12 @@ def report(request, report_name, username=None): if report_name == 'old': title = 'Packages last built more than one year ago' - cutoff = utc_now() - timedelta(days=365) + cutoff = now() - timedelta(days=365) packages = packages.filter( build_date__lt=cutoff).order_by('build_date') elif report_name == 'long-out-of-date': title = 'Packages marked out-of-date more than 90 days ago' - cutoff = utc_now() - timedelta(days=90) + cutoff = now() - timedelta(days=90) packages = packages.filter( flag_date__lt=cutoff).order_by('flag_date') elif report_name == 'big': @@ -199,7 +221,7 @@ def report(request, report_name, username=None): package.installed_size_pretty = filesizeformat( package.installed_size) ratio = package.compressed_size / float(package.installed_size) - package.ratio = '%.2f' % ratio + package.ratio = '%.3f' % ratio package.compress_type = package.filename.split('.')[-1] elif report_name == 'uncompressed-man': title = 'Packages with uncompressed manpages' @@ -214,7 +236,8 @@ def report(request, report_name, username=None): if username: pkg_ids = set(packages.values_list('id', flat=True)) bad_files = bad_files.filter(pkg__in=pkg_ids) - bad_files = bad_files.values_list('pkg_id', flat=True).distinct() + bad_files = bad_files.values_list( + 'pkg_id', flat=True).order_by().distinct() packages = packages.filter(id__in=set(bad_files)) elif report_name == 'uncompressed-info': title = 'Packages with uncompressed infopages' @@ -225,12 +248,13 @@ def report(request, report_name, username=None): if username: pkg_ids = set(packages.values_list('id', flat=True)) bad_files = bad_files.filter(pkg__in=pkg_ids) - bad_files = bad_files.values_list('pkg_id', flat=True).distinct() + bad_files = bad_files.values_list( + 'pkg_id', flat=True).order_by().distinct() packages = packages.filter(id__in=set(bad_files)) elif report_name == 'unneeded-orphans': title = 'Orphan packages required by no other packages' owned = PackageRelation.objects.all().values('pkgbase') - required = PackageDepend.objects.all().values('depname') + required = Depend.objects.all().values('name') # The two separate calls to exclude is required to do the right thing packages = packages.exclude(pkgbase__in=owned).exclude( pkgname__in=required) @@ -239,98 +263,52 @@ def report(request, report_name, username=None): names = [ 'Signature Date', 'Signed By', 'Packager' ] attrs = [ 'sig_date', 'sig_by', 'packager' ] cutoff = timedelta(hours=24) - finder = UserFinder() filtered = [] - packages = packages.filter(pgp_signature__isnull=False) + packages = packages.select_related( + 'arch', 'repo', 'packager').filter(pgp_signature__isnull=False) + known_keys = DeveloperKey.objects.select_related( + 'owner').filter(owner__isnull=False) + known_keys = {dk.key: dk for dk in known_keys} for package in packages: - sig_date = package.signature.datetime.replace(tzinfo=pytz.utc) + bad = False + sig = package.signature + sig_date = sig.creation_time.replace(tzinfo=pytz.utc) package.sig_date = sig_date.date() - key_id = package.signature.key_id - signer = finder.find_by_pgp_key(key_id) - package.sig_by = signer or key_id - if signer is None or signer.id != package.packager_id: - filtered.append(package) - elif sig_date > package.build_date + cutoff: + dev_key = known_keys.get(sig.key_id, None) + if dev_key: + package.sig_by = dev_key.owner + if dev_key.owner_id != package.packager_id: + bad = True + else: + package.sig_by = sig.key_id + bad = True + + if sig_date > package.build_date + cutoff: + bad = True + + if bad: filtered.append(package) packages = filtered else: raise Http404 + arches = {pkg.arch for pkg in packages} + repos = {pkg.repo for pkg in packages} context = { 'all_maintainers': maints, 'title': title, 'maintainer': user, 'packages': packages, + 'arches': sorted(arches), + 'repos': sorted(repos), 'column_names': names, 'column_attrs': attrs, } - return direct_to_template(request, 'devel/packages.html', context) - - -class NewUserForm(forms.ModelForm): - username = forms.CharField(max_length=30) - private_email = forms.EmailField() - first_name = forms.CharField(required=False) - last_name = forms.CharField(required=False) - groups = forms.ModelMultipleChoiceField(required=False, - queryset=Group.objects.all()) - - class Meta: - model = UserProfile - exclude = ('picture', 'user') - - def __init__(self, *args, **kwargs): - super(NewUserForm, self).__init__(*args, **kwargs) - # Hack ourself so certain fields appear first. self.fields is a - # SortedDict object where we can manipulate the keyOrder list. - order = self.fields.keyOrder - keys = ('username', 'private_email', 'first_name', 'last_name') - for key in reversed(keys): - order.remove(key) - order.insert(0, key) - - def clean_username(self): - username = self.cleaned_data['username'] - if User.objects.filter(username=username).exists(): - raise forms.ValidationError( - "A user with that username already exists.") - return username - - def save(self, commit=True): - profile = super(NewUserForm, self).save(False) - pwletters = ascii_letters + digits - password = ''.join([random.choice(pwletters) for _ in xrange(8)]) - user = User.objects.create_user(username=self.cleaned_data['username'], - email=self.cleaned_data['private_email'], password=password) - user.first_name = self.cleaned_data['first_name'] - user.last_name = self.cleaned_data['last_name'] - user.save() - # sucks that the MRM.add() method can't take a list directly... we have - # to resort to dirty * magic. - user.groups.add(*self.cleaned_data['groups']) - profile.user = user - if commit: - profile.save() - self.save_m2m() - - template = loader.get_template('devel/new_account.txt') - ctx = Context({ - 'site': Site.objects.get_current(), - 'user': user, - 'password': password, - }) - - send_mail("Your new "+settings.BRANDING_APPNAME+" account", - template.render(ctx), - settings.BRANDING_EMAIL, - [user.email], - fail_silently=False) + return render(request, 'devel/packages.html', context) + def log_addition(request, obj): """Cribbed from ModelAdmin.log_addition.""" - from django.contrib.admin.models import LogEntry, ADDITION - from django.contrib.contenttypes.models import ContentType - from django.utils.encoding import force_unicode LogEntry.objects.log_action( user_id = request.user.pk, content_type_id = ContentType.objects.get_for_model(obj).pk, @@ -340,17 +318,16 @@ def log_addition(request, obj): change_message = "Added via Create New User form." ) + @permission_required('auth.add_user') @never_cache def new_user_form(request): if request.POST: form = NewUserForm(request.POST) if form.is_valid(): - @transaction.commit_on_success - def inner_save(): + with transaction.commit_on_success(): form.save() log_addition(request, form.instance.user) - inner_save() return HttpResponseRedirect('/admin/auth/user/%d/' % \ form.instance.user.id) else: @@ -365,7 +342,8 @@ def new_user_form(request): 'title': 'Create User', 'submit_text': 'Create User' } - return direct_to_template(request, 'general_form.html', context) + return render(request, 'general_form.html', context) + @user_passes_test(lambda u: u.is_superuser) def admin_log(request, username=None): @@ -376,6 +354,6 @@ def admin_log(request, username=None): 'title': "Admin Action Log", 'log_user': user, } - return direct_to_template(request, 'devel/admin_log.html', context) + return render(request, 'devel/admin_log.html', context) # vim: set ts=4 sw=4 et: |