From 21461e78608bb687d7101dd55e72d44cbebf2ff6 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 9 Aug 2011 22:53:01 -0500 Subject: Add package details link tag Signed-off-by: Dan McGee --- packages/templatetags/package_extras.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'packages') diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py index e089b723..14a519d4 100644 --- a/packages/templatetags/package_extras.py +++ b/packages/templatetags/package_extras.py @@ -36,6 +36,11 @@ def do_buildsortqs(parser, token): "%r tag's argument should be in quotes" % tagname) return BuildQueryStringNode(sortfield[1:-1]) +@register.simple_tag +def pkg_details_link(pkg): + template = '%s' + return template % (pkg.get_absolute_url(), pkg.pkgname, pkg.pkgname) + @register.simple_tag def userpkgs(user): if user: @@ -48,7 +53,6 @@ def userpkgs(user): ) return '' - def svn_link(package, svnpath): '''Helper function for the two real SVN link methods.''' parts = (package.repo.svn_root, package.pkgbase, svnpath) -- cgit v1.2.3-54-g00ecf From d14e80e2f04edb2f52811dba805a7ed2aa680fab Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 9 Aug 2011 23:35:27 -0500 Subject: Add a template tag to link multiple packages at once Comma-separated list. Signed-off-by: Dan McGee --- packages/templatetags/package_extras.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'packages') diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py index 14a519d4..7bc868de 100644 --- a/packages/templatetags/package_extras.py +++ b/packages/templatetags/package_extras.py @@ -41,6 +41,10 @@ def pkg_details_link(pkg): template = '%s' return template % (pkg.get_absolute_url(), pkg.pkgname, pkg.pkgname) +@register.simple_tag +def multi_pkg_details(pkgs): + return ', '.join([pkg_details_link(pkg) for pkg in pkgs]) + @register.simple_tag def userpkgs(user): if user: -- cgit v1.2.3-54-g00ecf From 0df3567ae25bb2856bc62951844d9dab5ea97990 Mon Sep 17 00:00:00 2001 From: Sergej Pupykin Date: Sat, 13 Aug 2011 00:23:36 +0400 Subject: add "search wiki" link to package details page Dan: fix usage of urlencode() function. Signed-off-by: Dan McGee --- packages/templatetags/package_extras.py | 8 ++++++++ templates/packages/details.html | 1 + 2 files changed, 9 insertions(+) (limited to 'packages') diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py index 7bc868de..e4c7a010 100644 --- a/packages/templatetags/package_extras.py +++ b/packages/templatetags/package_extras.py @@ -72,6 +72,14 @@ def svn_arch(package): def svn_trunk(package): return svn_link(package, "trunk") +@register.simple_tag +def get_wiki_link(package): + data = { + 'search': package.pkgname, + } + return "https://wiki.archlinux.org/index.php/Special:Search?%s" % \ + urlencode(data) + @register.simple_tag def bugs_list(package): data = { diff --git a/templates/packages/details.html b/templates/packages/details.html index a3f2cef4..bec4bdff 100644 --- a/templates/packages/details.html +++ b/templates/packages/details.html @@ -18,6 +18,7 @@

Package Actions

  • SVN Entries ({{pkg.repo|lower}}-{{pkg.arch}})
  • SVN Entries (trunk)
  • +
  • Search Wiki
  • Bug Reports
  • Report a Bug
  • {% if pkg.flag_date %} -- cgit v1.2.3-54-g00ecf From c9c3dffdec1afa8ce4ff8d26113ff871a25d224d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 19 Aug 2011 18:32:17 -0500 Subject: Ensure ampersands are properly escaped in hrefs This was pointed out by the W3C validator. Signed-off-by: Dan McGee --- packages/templatetags/package_extras.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) (limited to 'packages') diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py index e4c7a010..42001aa5 100644 --- a/packages/templatetags/package_extras.py +++ b/packages/templatetags/package_extras.py @@ -9,6 +9,10 @@ register = template.Library() +def link_encode(url, query, doseq=False): + data = urlencode(query, doseq).replace('&', '&') + return "%s?%s" % (url, data) + class BuildQueryStringNode(template.Node): def __init__(self, sortfield): self.sortfield = sortfield @@ -22,7 +26,7 @@ def render(self, context): qs['sort'] = ['-' + self.sortfield] else: qs['sort'] = [self.sortfield] - return urlencode(qs, True) + return urlencode(qs, True).replace('&', '&') @register.tag(name='buildsortqs') def do_buildsortqs(parser, token): @@ -74,27 +78,29 @@ def svn_trunk(package): @register.simple_tag def get_wiki_link(package): + url = "https://wiki.archlinux.org/index.php/Special:Search" data = { 'search': package.pkgname, } - return "https://wiki.archlinux.org/index.php/Special:Search?%s" % \ - urlencode(data) + return link_encode(url, data) @register.simple_tag def bugs_list(package): + url = "https://bugs.archlinux.org/" data = { 'project': package.repo.bugs_project, 'string': package.pkgname, } - return "https://bugs.archlinux.org/?%s" % urlencode(data) + return link_encode(url, data) @register.simple_tag def bug_report(package): + url = "https://bugs.archlinux.org/newtask" data = { 'project': package.repo.bugs_project, 'product_category': package.repo.bugs_category, 'item_summary': '[%s]' % package.pkgname, } - return "https://bugs.archlinux.org/newtask?%s" % urlencode(data) + return link_encode(url, data) # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From c5b370f432215eb69dabd6bcb911b6429b16447a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 22 Aug 2011 16:42:06 -0500 Subject: Admin setup tweaks Signed-off-by: Dan McGee --- mirrors/admin.py | 3 ++- packages/admin.py | 3 ++- releng/admin.py | 4 ++-- releng/models.py | 6 +++++- 4 files changed, 11 insertions(+), 5 deletions(-) (limited to 'packages') diff --git a/mirrors/admin.py b/mirrors/admin.py index b7b478de..0632872d 100644 --- a/mirrors/admin.py +++ b/mirrors/admin.py @@ -59,7 +59,8 @@ class Meta: class MirrorAdmin(admin.ModelAdmin): form = MirrorAdminForm - list_display = ('name', 'tier', 'country', 'active', 'public', 'isos', 'admin_email', 'supported_protocols') + list_display = ('name', 'tier', 'country', 'active', 'public', + 'isos', 'admin_email') list_filter = ('tier', 'active', 'public', 'country') search_fields = ('name',) inlines = [ diff --git a/packages/admin.py b/packages/admin.py index 3ecfdbb1..01b6ed6c 100644 --- a/packages/admin.py +++ b/packages/admin.py @@ -3,8 +3,9 @@ from .models import PackageRelation class PackageRelationAdmin(admin.ModelAdmin): - list_display = ('user', 'pkgbase', 'type') + list_display = ('user', 'pkgbase', 'type', 'created') list_filter = ('type', 'user') + search_fields = ('user__username', 'pkgbase') admin.site.register(PackageRelation, PackageRelationAdmin) diff --git a/releng/admin.py b/releng/admin.py index be5e211f..e1411b84 100644 --- a/releng/admin.py +++ b/releng/admin.py @@ -5,8 +5,8 @@ Test) class IsoAdmin(admin.ModelAdmin): - list_display = ('name', 'created', 'active') - list_filter = ('active',) + list_display = ('name', 'created', 'active', 'removed') + list_filter = ('active', 'created') class TestAdmin(admin.ModelAdmin): list_display = ('user_name', 'user_email', 'created', 'ip_address', diff --git a/releng/models.py b/releng/models.py index a958288b..56187766 100644 --- a/releng/models.py +++ b/releng/models.py @@ -29,11 +29,15 @@ def get_absolute_url(self): def __unicode__(self): return self.name + class Meta: + verbose_name = 'ISO' + class Architecture(IsoOption): pass class IsoType(IsoOption): - pass + class Meta: + verbose_name = 'ISO type' class BootType(IsoOption): pass -- cgit v1.2.3-54-g00ecf From 25a15d4c570823c6e28693d68d57b803dc2673fa Mon Sep 17 00:00:00 2001 From: Evangelos Foutras Date: Thu, 1 Sep 2011 19:56:05 +0300 Subject: Use package branches to display commit history We now have one link pointing to the tree of /trunk, and another pointing to the log of /trunk. Both links specify a package branch. Signed-off-by: Evangelos Foutras Signed-off-by: Dan McGee --- packages/templatetags/package_extras.py | 19 ++++++------------- templates/packages/details.html | 6 ++++-- 2 files changed, 10 insertions(+), 15 deletions(-) (limited to 'packages') diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py index 42001aa5..01bf7510 100644 --- a/packages/templatetags/package_extras.py +++ b/packages/templatetags/package_extras.py @@ -61,20 +61,13 @@ def userpkgs(user): ) return '' -def svn_link(package, svnpath): - '''Helper function for the two real SVN link methods.''' - parts = (package.repo.svn_root, package.pkgbase, svnpath) - linkbase = "http://projects.archlinux.org/svntogit/%s.git/tree/%s/%s/" - return linkbase % tuple(urlquote(part) for part in parts) - @register.simple_tag -def svn_arch(package): - repo = package.repo.name.lower() - return svn_link(package, "repos/%s-%s" % (repo, package.arch.name)) - -@register.simple_tag -def svn_trunk(package): - return svn_link(package, "trunk") +def scm_link(package, operation): + parts = (package.repo.svn_root, operation, package.pkgbase) + linkbase = ( + "http://projects.archlinux.org/svntogit/%s.git/%s/trunk?" + "h=packages/%s") + return linkbase % tuple(urlquote(part) for part in parts) @register.simple_tag def get_wiki_link(package): diff --git a/templates/packages/details.html b/templates/packages/details.html index afbf9103..fa8283ed 100644 --- a/templates/packages/details.html +++ b/templates/packages/details.html @@ -16,8 +16,10 @@

    Package Details: {{ pkg.pkgname }} {{ pkg.full_version }}

    Package Actions

      -
    • SVN Entries ({{pkg.repo|lower}}-{{pkg.arch}})
    • -
    • SVN Entries (trunk)
    • +
    • + Source Files / + View Changes +
    • Search Wiki
    • Bug Reports
    • Report a Bug
    • -- cgit v1.2.3-54-g00ecf From b893682f356ca2861d676a51c4ae1c937d4c7c44 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 14 Sep 2011 14:37:13 -0400 Subject: Ensure we have a mirror URL to return If our query returned zero results, then try a slightly less exclusive query followed by returning a 404 result. Signed-off-by: Dan McGee --- packages/views.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'packages') diff --git a/packages/views.py b/packages/views.py index 8c9c1b18..79793355 100644 --- a/packages/views.py +++ b/packages/views.py @@ -548,15 +548,21 @@ def flag_confirmed(request, name, repo, arch): def download(request, name, repo, arch): pkg = get_object_or_404(Package, pkgname=name, repo__name__iexact=repo, arch__name=arch) - mirrorurl = MirrorUrl.objects.filter(mirror__country='Any', + mirror_urls = MirrorUrl.objects.filter( mirror__public=True, mirror__active=True, - protocol__protocol__iexact='HTTP')[0] + protocol__protocol__iexact='HTTP') + # look first for an 'Any' URL, then fall back to any HTTP URL + filtered_urls = mirror_urls.filter(mirror__country='Any')[:1] + if not filtered_urls: + filtered_urls = mirror_urls[:1] + if not filtered_urls: + raise Http404 arch = pkg.arch.name if pkg.arch.agnostic: # grab the first non-any arch to fake the download path arch = Arch.objects.exclude(agnostic=True)[0].name values = { - 'host': mirrorurl.url, + 'host': filtered_urls[0].url, 'arch': arch, 'repo': pkg.repo.name.lower(), 'file': pkg.filename, -- cgit v1.2.3-54-g00ecf From 797185faed0555efb88a1e6a18e447548a9935fd Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 19 Sep 2011 08:44:22 -0500 Subject: Add some dev dashboard info regarding signed package count This adds a column similar to the flagged package count for the number of signed packages in a given architecture or repository. It is up to the user to do some simple math to figure out the number of unsigned packages. Also, add 'signed' as a hidden search field option similar to what we did for packager. Signed-off-by: Dan McGee --- main/models.py | 4 ++++ packages/views.py | 8 ++++++++ templates/devel/index.html | 5 +++++ 3 files changed, 17 insertions(+) (limited to 'packages') diff --git a/main/models.py b/main/models.py index b5cd8638..ec1f0d2c 100644 --- a/main/models.py +++ b/main/models.py @@ -98,6 +98,10 @@ def flagged(self): """Used by dev dashboard.""" return self.filter(flag_date__isnull=False) + def signed(self): + """Used by dev dashboard.""" + return self.filter(pgp_signature__isnull=False) + def normal(self): return self.select_related('arch', 'repo') diff --git a/packages/views.py b/packages/views.py index 79793355..61e4d290 100644 --- a/packages/views.py +++ b/packages/views.py @@ -199,6 +199,9 @@ class PackageSearchForm(forms.Form): flagged = forms.ChoiceField( choices=[('', 'All')] + make_choice(['Flagged', 'Not Flagged']), required=False) + signed = forms.ChoiceField( + choices=[('', 'All')] + make_choice(['Signed', 'Unsigned']), + required=False) limit = LimitTypedChoiceField( choices=make_choice([50, 100, 250]) + [('all', 'All')], coerce=coerce_limit_value, @@ -254,6 +257,11 @@ def search(request, page=None): elif form.cleaned_data['flagged'] == 'Not Flagged': packages = packages.filter(flag_date__isnull=True) + if form.cleaned_data['signed'] == 'Signed': + packages = packages.filter(pgp_signature__isnull=False) + elif form.cleaned_data['signed'] == 'Unsigned': + packages = packages.filter(pgp_signature__isnull=True) + if form.cleaned_data['q']: query = form.cleaned_data['q'] q = Q(pkgname__icontains=query) | Q(pkgdesc__icontains=query) diff --git a/templates/devel/index.html b/templates/devel/index.html index 0c818d36..b2da70cf 100644 --- a/templates/devel/index.html +++ b/templates/devel/index.html @@ -135,6 +135,7 @@

      Stats by Architecture

      Arch # Packages # Flagged + # Signed @@ -147,6 +148,8 @@

      Stats by Architecture

      {{ arch.packages.flagged.count }} packages + + {{ arch.packages.signed.count }} packages {% endfor %} @@ -165,6 +168,7 @@

      Stats by Repository

      Repository # Packages # Flagged + # Signed @@ -177,6 +181,7 @@

      Stats by Repository

      {{ repo.packages.flagged.count }} packages + {{ repo.packages.signed.count }} packages {% endfor %} -- cgit v1.2.3-54-g00ecf From 71e57570c262fffb11ca6e0dc97342119198f740 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 11 Oct 2011 19:29:15 -0500 Subject: Pylint suggested and other cleanups Signed-off-by: Dan McGee --- main/middleware.py | 2 +- mirrors/admin.py | 5 +++-- news/views.py | 2 +- packages/models.py | 2 +- packages/views.py | 4 ++-- public/utils.py | 2 +- public/views.py | 11 +++++------ urls.py | 15 ++++++++------- visualize/views.py | 14 ++++++++++++-- 9 files changed, 34 insertions(+), 23 deletions(-) (limited to 'packages') diff --git a/main/middleware.py b/main/middleware.py index f893c795..f417b545 100644 --- a/main/middleware.py +++ b/main/middleware.py @@ -4,7 +4,7 @@ from django.conf import settings from django.core.cache import cache -from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers, get_max_age +from django.utils.cache import learn_cache_key, patch_response_headers, get_max_age class UpdateCacheMiddleware(object): """ diff --git a/mirrors/admin.py b/mirrors/admin.py index 0632872d..3786d8d2 100644 --- a/mirrors/admin.py +++ b/mirrors/admin.py @@ -33,14 +33,15 @@ class MirrorUrlInlineAdmin(admin.TabularInline): extra = 3 # ripped off from django.forms.fields, adding netmask ability -ipv4nm_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}(/(\d|[1-2]\d|3[0-2])){0,1}$') +IPV4NM_RE = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}(/(\d|[1-2]\d|3[0-2])){0,1}$') + class IPAddressNetmaskField(forms.fields.RegexField): default_error_messages = { 'invalid': u'Enter a valid IPv4 address, possibly including netmask.', } def __init__(self, *args, **kwargs): - super(IPAddressNetmaskField, self).__init__(ipv4nm_re, *args, **kwargs) + super(IPAddressNetmaskField, self).__init__(IPV4NM_RE, *args, **kwargs) class MirrorRsyncForm(forms.ModelForm): class Meta: diff --git a/news/views.py b/news/views.py index 990ee154..7ac009ba 100644 --- a/news/views.py +++ b/news/views.py @@ -32,7 +32,7 @@ def news_list(request): class NewsForm(forms.ModelForm): class Meta: model = News - exclude=('id', 'slug', 'author', 'postdate') + exclude = ('id', 'slug', 'author', 'postdate') def find_unique_slug(newsitem): '''Attempt to find a unique slug for this news item.''' diff --git a/packages/models.py b/packages/models.py index d2fe1878..4cd3b1b5 100644 --- a/packages/models.py +++ b/packages/models.py @@ -62,7 +62,7 @@ def packages(self): # TODO: delayed import to avoid circular reference from main.models import Package return Package.objects.normal().filter(pkgbase=self.pkgbase, - pkgver=self.pkgver, pkgrel=self.pkgrel, epoch=pkg.epoch, + pkgver=self.pkgver, pkgrel=self.pkgrel, epoch=self.epoch, arch=self.arch, repo=self.repo) @property diff --git a/packages/views.py b/packages/views.py index 61e4d290..a8216c7a 100644 --- a/packages/views.py +++ b/packages/views.py @@ -19,7 +19,7 @@ from datetime import datetime from operator import attrgetter -import string +from string import Template from urllib import urlencode from main.models import Package, PackageFile, Arch, Repo @@ -575,7 +575,7 @@ def download(request, name, repo, arch): 'repo': pkg.repo.name.lower(), 'file': pkg.filename, } - url = string.Template('${host}${repo}/os/${arch}/${file}').substitute(values) + url = Template('${host}${repo}/os/${arch}/${file}').substitute(values) return redirect(url) def arch_differences(request): diff --git a/public/utils.py b/public/utils.py index 5900c674..30c76ac1 100644 --- a/public/utils.py +++ b/public/utils.py @@ -1,6 +1,6 @@ from operator import attrgetter -from main.models import Arch, Package, Repo +from main.models import Arch, Package from main.utils import cache_function, groupby_preserve_order, PackageStandin class RecentUpdate(object): diff --git a/public/views.py b/public/views.py index 821e4d5c..14dd6353 100644 --- a/public/views.py +++ b/public/views.py @@ -5,7 +5,6 @@ from django.conf import settings from django.contrib.auth.models import User -from django.db.models import Q from django.http import Http404 from django.views.generic import list_detail from django.views.generic.simple import direct_to_template @@ -34,18 +33,18 @@ def index(request): }, } -def userlist(request, type='devs'): +def userlist(request, user_type='devs'): users = User.objects.order_by('username').select_related('userprofile') - if type == 'devs': + if user_type == 'devs': users = users.filter(is_active=True, groups__name="Developers") - elif type == 'tus': + elif user_type == 'tus': users = users.filter(is_active=True, groups__name="Trusted Users") - elif type == 'fellows': + elif user_type == 'fellows': users = users.filter(is_active=False, groups__name__in=["Developers", "Trusted Users"]) else: raise Http404 - context = USER_LISTS[type].copy() + context = USER_LISTS[user_type].copy() context['users'] = users return direct_to_template(request, 'public/userlist.html', context) diff --git a/urls.py b/urls.py index edd51b52..575910ea 100644 --- a/urls.py +++ b/urls.py @@ -1,6 +1,7 @@ import os.path -from django.conf.urls.defaults import * +# Stupid Django. Don't remove these "unused" handler imports +from django.conf.urls.defaults import handler500, handler404, include, patterns from django.conf import settings from django.contrib import admin @@ -11,7 +12,7 @@ from feeds import PackageFeed, NewsFeed import sitemaps -sitemaps = { +our_sitemaps = { 'base': sitemaps.BaseSitemap, 'news': sitemaps.NewsSitemap, 'packages': sitemaps.PackagesSitemap, @@ -36,9 +37,9 @@ # Sitemaps urlpatterns += patterns('django.contrib.sitemaps.views', (r'^sitemap.xml$', 'index', - {'sitemaps': sitemaps}), + {'sitemaps': our_sitemaps}), (r'^sitemap-(?P
      .+)\.xml$', 'sitemap', - {'sitemaps': sitemaps}), + {'sitemaps': our_sitemaps}), ) # Authentication / Admin @@ -58,9 +59,9 @@ {}, 'page-art'), (r'^svn/$', TemplateView.as_view(template_name='public/svn.html'), {}, 'page-svn'), - (r'^developers/$', 'userlist', { 'type':'devs' }, 'page-devs'), - (r'^trustedusers/$', 'userlist', { 'type':'tus' }, 'page-tus'), - (r'^fellows/$', 'userlist', { 'type':'fellows' }, 'page-fellows'), + (r'^developers/$', 'userlist', { 'user_type':'devs' }, 'page-devs'), + (r'^trustedusers/$', 'userlist', { 'user_type':'tus' }, 'page-tus'), + (r'^fellows/$', 'userlist', { 'user_type':'fellows' }, 'page-fellows'), (r'^donate/$', 'donate', {}, 'page-donate'), (r'^download/$', 'download', {}, 'page-download'), ) diff --git a/visualize/views.py b/visualize/views.py index 68f5d4a5..f2b1d63b 100644 --- a/visualize/views.py +++ b/visualize/views.py @@ -18,10 +18,20 @@ def arch_repo_data(): arches = Arch.objects.values_list('name', flat=True) repos = Repo.objects.values_list('name', flat=True) + def build_map(name, arch, repo): + key = '%s:%s' % (repo or '', arch or '') + return { + 'key': key, + 'name': name, + 'arch': arch, + 'repo': repo, + 'data': [], + } + # now transform these results into two mappings: one ordered (repo, arch), # and one ordered (arch, repo). - arch_groups = dict((a, { 'name': a, 'key': ':%s' % a, 'arch': a, 'repo': None, 'data': [] }) for a in arches) - repo_groups = dict((r, { 'name': r, 'key': '%s:' % r, 'arch': None, 'repo': r, 'data': [] }) for r in repos) + arch_groups = dict((a, build_map(a, a, None)) for a in arches) + repo_groups = dict((r, build_map(r, None, r)) for r in repos) for row in qs: arch = row['arch__name'] repo = row['repo__name'] -- cgit v1.2.3-54-g00ecf From 0d693fa1fb788a61359415f56dc487f4aa504a55 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 12 Oct 2011 08:47:26 -0500 Subject: Add hidden name and desc fields to package search Not linked from anywhere just yet, but they are available if you know they exist and can be used in the standard query string. Signed-off-by: Dan McGee --- packages/views.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) (limited to 'packages') diff --git a/packages/views.py b/packages/views.py index a8216c7a..a740e689 100644 --- a/packages/views.py +++ b/packages/views.py @@ -191,6 +191,8 @@ def valid_value(self, value): class PackageSearchForm(forms.Form): repo = forms.MultipleChoiceField(required=False) arch = forms.MultipleChoiceField(required=False) + name = forms.CharField(required=False) + desc = forms.CharField(required=False) q = forms.CharField(required=False) maintainer = forms.ChoiceField(required=False) packager = forms.ChoiceField(required=False) @@ -262,15 +264,24 @@ def search(request, page=None): elif form.cleaned_data['signed'] == 'Unsigned': packages = packages.filter(pgp_signature__isnull=True) - if form.cleaned_data['q']: - query = form.cleaned_data['q'] - q = Q(pkgname__icontains=query) | Q(pkgdesc__icontains=query) - packages = packages.filter(q) if form.cleaned_data['last_update']: lu = form.cleaned_data['last_update'] packages = packages.filter(last_update__gte= datetime(lu.year, lu.month, lu.day, 0, 0)) + if form.cleaned_data['name']: + name = form.cleaned_data['name'] + packages = packages.filter(pkgname__icontains=name) + + if form.cleaned_data['desc']: + desc = form.cleaned_data['desc'] + packages = packages.filter(pkgdesc__icontains=desc) + + if form.cleaned_data['q']: + query = form.cleaned_data['q'] + q = Q(pkgname__icontains=query) | Q(pkgdesc__icontains=query) + packages = packages.filter(q) + asked_limit = form.cleaned_data['limit'] if asked_limit and asked_limit < 0: limit = None -- cgit v1.2.3-54-g00ecf From 21a8fec980eb0613d3cce8aae7d6bfe6680c038a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 12 Oct 2011 08:54:51 -0500 Subject: Package search and sort code cleanup Move initializations closer to where they are actually needed, and remove the sorting on multiple columns when a sort field is passed in. We don't do this for the default sort, so let's not do it here either. Signed-off-by: Dan McGee --- packages/views.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) (limited to 'packages') diff --git a/packages/views.py b/packages/views.py index a740e689..dab06919 100644 --- a/packages/views.py +++ b/packages/views.py @@ -293,24 +293,22 @@ def search(request, page=None): else: form = PackageSearchForm() - current_query = request.GET.urlencode() - page_dict = { - 'search_form': form, - 'current_query': current_query - } allowed_sort = ["arch", "repo", "pkgname", "pkgbase", "compressed_size", "installed_size", "build_date", "last_update", "flag_date"] allowed_sort += ["-" + s for s in allowed_sort] sort = request.GET.get('sort', None) - # TODO: sorting by multiple fields makes using a DB index much harder if sort in allowed_sort: - packages = packages.order_by( - request.GET['sort'], 'repo', 'arch', 'pkgname') + packages = packages.order_by(sort) page_dict['sort'] = sort else: packages = packages.order_by('pkgname') + current_query = request.GET.urlencode() + page_dict = { + 'search_form': form, + 'current_query': current_query + } return list_detail.object_list(request, packages, template_name="packages/search.html", page=page, -- cgit v1.2.3-54-g00ecf From 16cac1ad88cb2fb653cf7abcb91e6f51c7510e27 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 12 Oct 2011 09:03:28 -0500 Subject: Revert movement of search initialization code I'm stupid and didn't realize it was referenced before the location I moved it to. Signed-off-by: Dan McGee --- packages/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'packages') diff --git a/packages/views.py b/packages/views.py index dab06919..5114c87f 100644 --- a/packages/views.py +++ b/packages/views.py @@ -293,6 +293,11 @@ def search(request, page=None): else: form = PackageSearchForm() + current_query = request.GET.urlencode() + page_dict = { + 'search_form': form, + 'current_query': current_query + } allowed_sort = ["arch", "repo", "pkgname", "pkgbase", "compressed_size", "installed_size", "build_date", "last_update", "flag_date"] @@ -304,11 +309,6 @@ def search(request, page=None): else: packages = packages.order_by('pkgname') - current_query = request.GET.urlencode() - page_dict = { - 'search_form': form, - 'current_query': current_query - } return list_detail.object_list(request, packages, template_name="packages/search.html", page=page, -- cgit v1.2.3-54-g00ecf From ac2278423a3d449fdfe8c813f1f2d391ef9aff08 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 14:59:00 -0500 Subject: Many signoff page improvements Add a new 'SignoffSpecification' model which will capture metadata regarding a specific package if it differs from the norm- e.g. more or less than 2 required signoffs, is known to be bad, a comment from the maintainer, etc. The groundwork is laid here; much of this will still need to be wired up in the future. Enhance the view with a lot more JS prettiness and add revoking of signoffs. The signoff page can be filtered and the links and all the fun stuff are totally dynamic now. Signed-off-by: Dan McGee --- main/models.py | 11 -- media/archweb.css | 1 - media/archweb.js | 72 ++++++-- .../0010_auto__add_signoffspecification.py | 183 +++++++++++++++++++++ packages/models.py | 45 ++++- packages/urls.py | 1 + packages/views.py | 71 +++++--- templates/packages/differences.html | 2 +- templates/packages/signoff_cell.html | 12 ++ templates/packages/signoffs.html | 50 +++--- 10 files changed, 379 insertions(+), 69 deletions(-) create mode 100644 packages/migrations/0010_auto__add_signoffspecification.py create mode 100644 templates/packages/signoff_cell.html (limited to 'packages') diff --git a/main/models.py b/main/models.py index 780453c0..d55a9673 100644 --- a/main/models.py +++ b/main/models.py @@ -7,7 +7,6 @@ from main.utils import cache_function, make_choice, set_created_field from packages.models import PackageRelation -from packages.models import Signoff as PackageSignoff from datetime import datetime from itertools import groupby @@ -213,16 +212,6 @@ def maintainers(self): package_relations__pkgbase=self.pkgbase, package_relations__type=PackageRelation.MAINTAINER) - @property - def signoffs(self): - return PackageSignoff.objects.select_related('user').filter( - pkgbase=self.pkgbase, pkgver=self.pkgver, pkgrel=self.pkgrel, - epoch=self.epoch, arch=self.arch, repo=self.repo) - - def approved_for_signoff(self): - count = self.signoffs.filter(revoked__isnull=True).count() - return count >= PackageSignoff.REQUIRED - @cache_function(300) def applicable_arches(self): '''The list of (this arch) + (available agnostic arches).''' diff --git a/media/archweb.css b/media/archweb.css index ea2f3fb5..62dc4fbc 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -912,7 +912,6 @@ ul.admin-actions { #dev-signoffs .signed-username { color: #888; - margin-left: 0.5em; } /* iso testing feedback form */ diff --git a/media/archweb.js b/media/archweb.js index a51ae460..43812b33 100644 --- a/media/archweb.js +++ b/media/archweb.js @@ -139,7 +139,7 @@ function ajaxifyFiles() { /* packages/differences.html */ function filter_packages() { - // start with all rows, and then remove ones we shouldn't show + /* start with all rows, and then remove ones we shouldn't show */ var rows = $('#tbody_differences').children(); var all_rows = rows; if (!$('#id_multilib').is(':checked')) { @@ -150,12 +150,12 @@ function filter_packages() { rows = rows.filter('.' + arch); } if (!$('#id_minor').is(':checked')) { - // this check is done last because it is the most expensive + /* this check is done last because it is the most expensive */ var pat = /(.*)-(.+)/; rows = rows.filter(function(index) { var cells = $(this).children('td'); - // all this just to get the split version out of the table cell + /* all this just to get the split version out of the table cell */ var ver_a = cells.eq(2).find('span').text().match(pat); if (!ver_a) { return true; @@ -166,26 +166,26 @@ function filter_packages() { return true; } - // first check pkgver + /* first check pkgver */ if (ver_a[1] !== ver_b[1]) { return true; } - // pkgver matched, so see if rounded pkgrel matches + /* pkgver matched, so see if rounded pkgrel matches */ if (Math.floor(parseFloat(ver_a[2])) === Math.floor(parseFloat(ver_b[2]))) { return false; } - // pkgrel didn't match, so keep the row + /* pkgrel didn't match, so keep the row */ return true; }); } - // hide all rows, then show the set we care about + /* hide all rows, then show the set we care about */ all_rows.hide(); rows.show(); - // make sure we update the odd/even styling from sorting + /* make sure we update the odd/even styling from sorting */ $('.results').trigger('applyWidgets'); } -function filter_reset() { +function filter_packages_reset() { $('#id_archonly').val('both'); $('#id_multilib').removeAttr('checked'); $('#id_minor').removeAttr('checked'); @@ -213,26 +213,72 @@ function todolist_flag() { function signoff_package() { var link = this; $.getJSON(link.href, function(data) { + link = $(link); + var signoff = null; if (data.created) { - var signoff = $('
    • ').addClass('signed-username').text(data.user); - $(link).append(signoff); + signoff = $('
    • ').addClass('signed-username').text(data.user); + link.closest('td').children('ul').append(signoff); + } else if(data.user) { + signoff = link.closest('td').find('li').filter(function(index) { + return $(this).text() == data.user; + }); + } + console.log(signoff, data.revoked, data.user); + if (signoff && data.revoked) { + signoff.text(signoff.text() + ' (revoked)'); } /* update the approved column to reflect reality */ var approved; if (data.approved) { - approved = $(link).closest('tr').children('.signoff-no'); + approved = link.closest('tr').children('.signoff-no'); approved.text('Yes').addClass( 'signoff-yes').removeClass('signoff-no'); } else { - approved = $(link).closest('tr').children('.signoff-yes'); + approved = link.closest('tr').children('.signoff-yes'); approved.text('No').addClass( 'signoff-no').removeClass('signoff-yes'); } + link.removeAttr('title'); + /* Form our new link. The current will be something like + * '/packages/repo/arch/package/...' */ + var base_href = link.attr('href').split('/').slice(0, 5).join('/'); + if (data.revoked) { + link.text('Signoff'); + link.attr('href', base_href + '/signoff/'); + } else { + link.text('Revoke Signoff'); + link.attr('href', base_href + '/signoff/revoke/'); + } $('.results').trigger('updateCell', approved); }); return false; } +function filter_signoffs() { + /* start with all rows, and then remove ones we shouldn't show */ + var rows = $('#tbody_signoffs').children(); + var all_rows = rows; + $('#signoffs_filter .arch_filter').each(function() { + if (!$(this).is(':checked')) { + console.log($(this).val()); + rows = rows.not('.' + $(this).val()); + } + }); + if ($('#id_pending').is(':checked')) { + rows = rows.has('td.signoff-no'); + } + /* hide all rows, then show the set we care about */ + all_rows.hide(); + rows.show(); + /* make sure we update the odd/even styling from sorting */ + $('.results').trigger('applyWidgets'); +} +function filter_signoffs_reset() { + $('#signoffs_filter .arch_filter').attr('checked', 'checked'); + $('#id_pending').removeAttr('checked'); + filter_signoffs(); +} + /* visualizations */ function format_filesize(size, decimals) { /*var labels = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];*/ diff --git a/packages/migrations/0010_auto__add_signoffspecification.py b/packages/migrations/0010_auto__add_signoffspecification.py new file mode 100644 index 00000000..da24824e --- /dev/null +++ b/packages/migrations/0010_auto__add_signoffspecification.py @@ -0,0 +1,183 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.create_table('packages_signoffspecification', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('pkgbase', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('pkgver', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('pkgrel', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('epoch', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), + ('arch', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['main.Arch'])), + ('repo', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['main.Repo'])), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('created', self.gf('django.db.models.fields.DateTimeField')()), + ('required', self.gf('django.db.models.fields.PositiveIntegerField')(default=2)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('known_bad', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('comments', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('packages', ['SignoffSpecification']) + + + def backwards(self, orm): + db.delete_table('packages_signoffspecification') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'main.arch': { + 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"}, + 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'main.package': { + 'Meta': {'ordering': "('pkgname',)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.models.PositiveBigIntegerField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'files_last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'flag_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'installed_size': ('main.models.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Repo']"}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) + }, + 'main.repo': { + 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"}, + 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'staging': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'svn_root': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'testing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'packages.conflict': { + 'Meta': {'ordering': "['name']", 'object_name': 'Conflict'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'conflicts'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.license': { + 'Meta': {'ordering': "['name']", 'object_name': 'License'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'licenses'", 'to': "orm['main.Package']"}) + }, + 'packages.packagegroup': { + 'Meta': {'object_name': 'PackageGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Package']"}) + }, + 'packages.packagerelation': { + 'Meta': {'unique_together': "(('pkgbase', 'user', 'type'),)", 'object_name': 'PackageRelation'}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_relations'", 'to': "orm['auth.User']"}) + }, + 'packages.provision': { + 'Meta': {'ordering': "['name']", 'object_name': 'Provision'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'provides'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.replacement': { + 'Meta': {'ordering': "['name']", 'object_name': 'Replacement'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'replaces'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index 4cd3b1b5..ad082501 100644 --- a/packages/models.py +++ b/packages/models.py @@ -38,6 +38,49 @@ def __unicode__(self): class Meta: unique_together = (('pkgbase', 'user', 'type'),) +class SignoffSpecification(models.Model): + ''' + A specification for the signoff policy for this particular revision of a + pakcage. The default is requiring two signoffs for a given package. These + are created only if necessary; e.g., if one wanted to override the + required=2 attribute, otherwise a sane default object is used. + ''' + pkgbase = models.CharField(max_length=255, db_index=True) + pkgver = models.CharField(max_length=255) + pkgrel = models.CharField(max_length=255) + epoch = models.PositiveIntegerField(default=0) + arch = models.ForeignKey('main.Arch') + repo = models.ForeignKey('main.Repo') + user = models.ForeignKey(User) + created = models.DateTimeField(editable=False) + required = models.PositiveIntegerField(default=2) + enabled = models.BooleanField(default=True) + known_bad = models.BooleanField(default=False) + comments = models.TextField(null=True, blank=True) + +class SignoffManager(models.Manager): + def get_from_package(self, pkg, user, revoked=False): + '''Utility method to pull all relevant name-version fields from a + package and create a matching signoff.''' + not_revoked = not revoked + return Signoff.objects.get( + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo, + revoked__isnull=not_revoked, user=user) + + def get_or_create_from_package(self, pkg, user): + '''Utility method to pull all relevant name-version fields from a + package and create a matching signoff.''' + return Signoff.objects.get_or_create( + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo, + revoked=None, user=user) + + def for_package(self, pkg): + return self.select_related('user').filter( + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo) + class Signoff(models.Model): ''' A signoff for a package (by pkgbase) at a given point in time. These are @@ -55,7 +98,7 @@ class Signoff(models.Model): revoked = models.DateTimeField(null=True) comments = models.TextField(null=True, blank=True) - REQUIRED = 2 + objects = SignoffManager() @property def packages(self): diff --git a/packages/urls.py b/packages/urls.py index d7d01170..576e3279 100644 --- a/packages/urls.py +++ b/packages/urls.py @@ -10,6 +10,7 @@ (r'^unflag/$', 'unflag'), (r'^unflag/all/$', 'unflag_all'), (r'^signoff/$', 'signoff_package'), + (r'^signoff/revoke/$', 'signoff_package', {'revoke': True}), (r'^download/$', 'download'), ) diff --git a/packages/views.py b/packages/views.py index 5114c87f..035d51cb 100644 --- a/packages/views.py +++ b/packages/views.py @@ -25,7 +25,7 @@ from main.models import Package, PackageFile, Arch, Repo from main.utils import make_choice, groupby_preserve_order, PackageStandin from mirrors.models import MirrorUrl -from .models import PackageRelation, PackageGroup, Signoff +from .models import PackageRelation, PackageGroup, SignoffSpecification, Signoff from .utils import (get_group_info, get_differences_info, get_wrong_permissions, get_current_signoffs) @@ -369,14 +369,24 @@ def unflag_all(request, name, repo, arch): pkgs.update(flag_date=None) return redirect(pkg) +DEFAULT_SIGNOFF_SPEC = SignoffSpecification(required=2) + +def approved_by_signoffs(signoffs, spec=DEFAULT_SIGNOFF_SPEC): + if signoffs: + good_signoffs = sum(1 for s in signoffs if not s.revoked) + return good_signoffs >= spec.required + return False + class PackageSignoffGroup(object): '''Encompasses all packages in testing with the same pkgbase.''' - def __init__(self, packages, target_repo=None, signoffs=None): + def __init__(self, packages, user=None): if len(packages) == 0: raise Exception self.packages = packages - self.target_repo = target_repo - self.signoffs = signoffs + self.user = user + self.target_repo = None + self.signoffs = set() + self.specification = DEFAULT_SIGNOFF_SPEC first = packages[0] self.pkgbase = first.pkgbase @@ -406,21 +416,24 @@ def package(self): def find_signoffs(self, all_signoffs): '''Look through a list of Signoff objects for ones matching this particular group and store them on the object.''' - if self.signoffs is None: - self.signoffs = [] for s in all_signoffs: if s.pkgbase != self.pkgbase: continue if self.version and not s.full_version == self.version: continue if s.arch_id == self.arch.id and s.repo_id == self.repo.id: - self.signoffs.append(s) + self.signoffs.add(s) def approved(self): - if self.signoffs: - good_signoffs = [s for s in self.signoffs if not s.revoked] - return len(good_signoffs) >= Signoff.REQUIRED - return False + return approved_by_signoffs(self.signoffs, self.specification) + + def user_signed_off(self, user=None): + '''Did a given user signoff on this package? user can be passed as an + argument, or attached to the group object itself so this can be called + from a template.''' + if user is None: + user = self.user + return user in (s.user for s in self.signoffs if not s.revoked) @permission_required('main.change_package') @never_cache @@ -443,7 +456,7 @@ def signoffs(request): grouped = groupby_preserve_order(packages, same_pkgbase_key) signoff_groups = [] for group in grouped: - signoff_group = PackageSignoffGroup(group) + signoff_group = PackageSignoffGroup(group, user=request.user) signoff_group.target_repo = pkgtorepo.get(signoff_group.pkgbase, "Unknown") signoff_group.find_signoffs(signoffs) @@ -451,27 +464,43 @@ def signoffs(request): signoff_groups.sort(key=attrgetter('pkgbase')) - return direct_to_template(request, 'packages/signoffs.html', - {'signoff_groups': signoff_groups}) + context = { + 'signoff_groups': signoff_groups, + 'arches': Arch.objects.all(), + } + return direct_to_template(request, 'packages/signoffs.html', context) @permission_required('main.change_package') @never_cache -def signoff_package(request, name, repo, arch): +def signoff_package(request, name, repo, arch, revoke=False): packages = get_list_or_404(Package, pkgbase=name, arch__name=arch, repo__name__iexact=repo, repo__testing=True) - pkg = packages[0] - signoff, created = Signoff.objects.get_or_create( - pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, - epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo, user=request.user) + package = packages[0] + + if revoke: + try: + signoff = Signoff.objects.get_from_package( + package, request.user, False) + except Signoff.DoesNotExist: + raise Http404 + signoff.revoked = datetime.utcnow() + signoff.save() + created = False + else: + signoff, created = Signoff.objects.get_or_create_from_package( + package, request.user) + + all_signoffs = Signoff.objects.for_package(package) if request.is_ajax(): data = { 'created': created, - 'approved': pkg.approved_for_signoff(), + 'revoked': bool(signoff.revoked), + 'approved': approved_by_signoffs(all_signoffs), 'user': str(request.user), } - return HttpResponse(simplejson.dumps(data), + return HttpResponse(simplejson.dumps(data, ensure_ascii=False), mimetype='application/json') return redirect('package-signoffs') diff --git a/templates/packages/differences.html b/templates/packages/differences.html index dd1046bc..0400ea37 100644 --- a/templates/packages/differences.html +++ b/templates/packages/differences.html @@ -65,7 +65,7 @@

      Filter Differences View

      $('.results').tablesorter({widgets: ['zebra'], sortList: [[1,0], [0,0]]}); $('#diff_filter select').change(filter_packages); $('#diff_filter input').change(filter_packages); - $('#criteria_reset').click(filter_reset); + $('#criteria_reset').click(filter_differences_reset); // fire function on page load to ensure the current form selections take effect filter_packages(); }); diff --git a/templates/packages/signoff_cell.html b/templates/packages/signoff_cell.html new file mode 100644 index 00000000..fce5d551 --- /dev/null +++ b/templates/packages/signoff_cell.html @@ -0,0 +1,12 @@ +
        + {% for signoff in group.signoffs %} +
      • {{ signoff.user }}{% if signoff.revoked %} (revoked){% endif %}
      • + {% endfor %} +
      +{% if group.user_signed_off %} + +{% else %} + +{% endif %} diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index a8aa4de2..4a2f6c99 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -12,42 +12,46 @@

      Package Signoffs

      {{ signoff_groups|length }} signoff group{{ signoff_groups|pluralize }} found. A "signoff group" consists of packages grouped by pkgbase, architecture, and repository.

      +
      +

      Filter Displayed Signoffs

      +
      +
      + Select filter criteria + {% for arch in arches %} +
      +
      + {% endfor %} +
      +
      +
      +
      +
      +
      + + - + - - - + - + {% for group in signoff_groups %} {% with group.package as pkg %} - + + - + - - - + {% endwith %} {% endfor %} @@ -60,8 +64,12 @@

      Package Signoffs

      {% endblock %} -- cgit v1.2.3-54-g00ecf From 74d2a5df5ca7ee4b6497a6e7609491d72cdbb309 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 17:18:13 -0500 Subject: Refactor more package signoff stuff This sets up some shared utility code for use in a later package signoff email report command. Signed-off-by: Dan McGee --- packages/models.py | 7 +- packages/utils.py | 134 ++++++++++++++++++++++++++++++++--- packages/views.py | 100 ++------------------------ templates/packages/signoff_cell.html | 4 +- templates/packages/signoffs.html | 12 ++-- 5 files changed, 144 insertions(+), 113 deletions(-) (limited to 'packages') diff --git a/packages/models.py b/packages/models.py index ad082501..3c319fe7 100644 --- a/packages/models.py +++ b/packages/models.py @@ -115,8 +115,11 @@ def full_version(self): return u'%s-%s' % (self.pkgver, self.pkgrel) def __unicode__(self): - return u'%s-%s: %s' % ( - self.pkgbase, self.full_version, self.user) + revoked = u'' + if self.revoked: + revoked = u' (revoked)' + return u'%s-%s: %s%s' % ( + self.pkgbase, self.full_version, self.user, revoked) class PackageGroup(models.Model): ''' diff --git a/packages/utils.py b/packages/utils.py index c8c1f8a6..42cfbe0f 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -1,12 +1,11 @@ -from collections import defaultdict from operator import itemgetter from django.db import connection from django.db.models import Count, Max -from main.models import Package -from main.utils import cache_function -from .models import PackageGroup, PackageRelation, Signoff +from main.models import Package, Repo +from main.utils import cache_function, groupby_preserve_order, PackageStandin +from .models import PackageGroup, PackageRelation, SignoffSpecification, Signoff @cache_function(300) def get_group_info(include_arches=None): @@ -148,8 +147,90 @@ def get_wrong_permissions(): id__in=to_fetch) return relations -def get_current_signoffs(): - '''Returns a mapping of pkgbase -> signoff objects.''' + +DEFAULT_SIGNOFF_SPEC = SignoffSpecification() + +def approved_by_signoffs(signoffs, spec=DEFAULT_SIGNOFF_SPEC): + if signoffs: + good_signoffs = sum(1 for s in signoffs if not s.revoked) + return good_signoffs >= spec.required + return False + +class PackageSignoffGroup(object): + '''Encompasses all packages in testing with the same pkgbase.''' + def __init__(self, packages, user=None): + if len(packages) == 0: + raise Exception + self.packages = packages + self.user = user + self.target_repo = None + self.signoffs = set() + self.specification = DEFAULT_SIGNOFF_SPEC + + first = packages[0] + self.pkgbase = first.pkgbase + self.arch = first.arch + self.repo = first.repo + self.version = '' + self.last_update = first.last_update + self.packager = first.packager + + version = first.full_version + if all(version == pkg.full_version for pkg in packages): + self.version = version + + @property + def package(self): + '''Try and return a relevant single package object representing this + group. Start by seeing if there is only one package, then look for the + matching package by name, finally falling back to a standin package + object.''' + if len(self.packages) == 1: + return self.packages[0] + + same_pkgs = [p for p in self.packages if p.pkgname == p.pkgbase] + if same_pkgs: + return same_pkgs[0] + + return PackageStandin(self.packages[0]) + + def find_signoffs(self, all_signoffs): + '''Look through a list of Signoff objects for ones matching this + particular group and store them on the object.''' + for s in all_signoffs: + if s.pkgbase != self.pkgbase: + continue + if self.version and not s.full_version == self.version: + continue + if s.arch_id == self.arch.id and s.repo_id == self.repo.id: + self.signoffs.add(s) + + def approved(self): + return approved_by_signoffs(self.signoffs, self.specification) + + @property + def completed(self): + return sum(1 for s in self.signoffs if not s.revoked) + + @property + def required(self): + return self.specification.required + + def user_signed_off(self, user=None): + '''Did a given user signoff on this package? user can be passed as an + argument, or attached to the group object itself so this can be called + from a template.''' + if user is None: + user = self.user + return user in (s.user for s in self.signoffs if not s.revoked) + + def __unicode__(self): + return u'%s-%s (%s): %d' % ( + self.pkgbase, self.version, self.arch, len(self.signoffs)) + +def get_current_signoffs(repos): + '''Returns a mapping of pkgbase -> signoff objects for the given repos.''' + cursor = connection.cursor() sql = """ SELECT DISTINCT s.id FROM packages_signoff s @@ -162,14 +243,49 @@ def get_current_signoffs(): AND s.repo_id = p.repo_id ) JOIN repos r ON p.repo_id = r.id - WHERE r.testing = %s + WHERE r.id IN ( """ - cursor = connection.cursor() - cursor.execute(sql, [True]) + sql += ", ".join("%s" for r in repos) + sql += ")" + cursor.execute(sql, [r.id for r in repos]) + results = cursor.fetchall() # fetch all of the returned signoffs by ID to_fetch = [row[0] for row in results] signoffs = Signoff.objects.select_related('user').in_bulk(to_fetch) return signoffs.values() +def get_target_repo_map(pkgbases): + package_repos = Package.objects.order_by().values_list( + 'pkgbase', 'repo__name').filter( + repo__testing=False, repo__staging=False, + pkgbase__in=pkgbases).distinct() + return dict(package_repos) + +def get_signoff_groups(repos=None): + if repos is None: + repos = Repo.objects.filter(testing=True) + + test_pkgs = Package.objects.normal().filter(repo__in=repos) + packages = test_pkgs.order_by('pkgname') + + # Collect all pkgbase values in testing repos + q_pkgbase = test_pkgs.values('pkgbase') + pkgtorepo = get_target_repo_map(q_pkgbase) + + # Collect all existing signoffs for these packages + signoffs = get_current_signoffs(repos) + + same_pkgbase_key = lambda x: (x.repo.name, x.arch.name, x.pkgbase) + grouped = groupby_preserve_order(packages, same_pkgbase_key) + signoff_groups = [] + for group in grouped: + signoff_group = PackageSignoffGroup(group) + signoff_group.target_repo = pkgtorepo.get(signoff_group.pkgbase, + "Unknown") + signoff_group.find_signoffs(signoffs) + signoff_groups.append(signoff_group) + + return signoff_groups + # vim: set ts=4 sw=4 et: diff --git a/packages/views.py b/packages/views.py index 035d51cb..e102760b 100644 --- a/packages/views.py +++ b/packages/views.py @@ -23,11 +23,11 @@ from urllib import urlencode from main.models import Package, PackageFile, Arch, Repo -from main.utils import make_choice, groupby_preserve_order, PackageStandin +from main.utils import make_choice from mirrors.models import MirrorUrl -from .models import PackageRelation, PackageGroup, SignoffSpecification, Signoff +from .models import PackageRelation, PackageGroup, Signoff from .utils import (get_group_info, get_differences_info, - get_wrong_permissions, get_current_signoffs) + get_wrong_permissions, get_signoff_groups, approved_by_signoffs) class PackageJSONEncoder(DjangoJSONEncoder): pkg_attributes = [ 'pkgname', 'pkgbase', 'repo', 'arch', 'pkgver', @@ -369,100 +369,12 @@ def unflag_all(request, name, repo, arch): pkgs.update(flag_date=None) return redirect(pkg) -DEFAULT_SIGNOFF_SPEC = SignoffSpecification(required=2) - -def approved_by_signoffs(signoffs, spec=DEFAULT_SIGNOFF_SPEC): - if signoffs: - good_signoffs = sum(1 for s in signoffs if not s.revoked) - return good_signoffs >= spec.required - return False - -class PackageSignoffGroup(object): - '''Encompasses all packages in testing with the same pkgbase.''' - def __init__(self, packages, user=None): - if len(packages) == 0: - raise Exception - self.packages = packages - self.user = user - self.target_repo = None - self.signoffs = set() - self.specification = DEFAULT_SIGNOFF_SPEC - - first = packages[0] - self.pkgbase = first.pkgbase - self.arch = first.arch - self.repo = first.repo - self.version = '' - - version = first.full_version - if all(version == pkg.full_version for pkg in packages): - self.version = version - - @property - def package(self): - '''Try and return a relevant single package object representing this - group. Start by seeing if there is only one package, then look for the - matching package by name, finally falling back to a standin package - object.''' - if len(self.packages) == 1: - return self.packages[0] - - same_pkgs = [p for p in self.packages if p.pkgname == p.pkgbase] - if same_pkgs: - return same_pkgs[0] - - return PackageStandin(self.packages[0]) - - def find_signoffs(self, all_signoffs): - '''Look through a list of Signoff objects for ones matching this - particular group and store them on the object.''' - for s in all_signoffs: - if s.pkgbase != self.pkgbase: - continue - if self.version and not s.full_version == self.version: - continue - if s.arch_id == self.arch.id and s.repo_id == self.repo.id: - self.signoffs.add(s) - - def approved(self): - return approved_by_signoffs(self.signoffs, self.specification) - - def user_signed_off(self, user=None): - '''Did a given user signoff on this package? user can be passed as an - argument, or attached to the group object itself so this can be called - from a template.''' - if user is None: - user = self.user - return user in (s.user for s in self.signoffs if not s.revoked) - @permission_required('main.change_package') @never_cache def signoffs(request): - test_pkgs = Package.objects.normal().filter(repo__testing=True) - packages = test_pkgs.order_by('pkgname') - - # Collect all pkgbase values in testing repos - q_pkgbase = test_pkgs.values('pkgbase') - package_repos = Package.objects.order_by().values_list( - 'pkgbase', 'repo__name').filter( - repo__testing=False, repo__staging=False, - pkgbase__in=q_pkgbase).distinct() - pkgtorepo = dict(package_repos) - - # Collect all existing signoffs for these packages - signoffs = get_current_signoffs() - - same_pkgbase_key = lambda x: (x.repo.name, x.arch.name, x.pkgbase) - grouped = groupby_preserve_order(packages, same_pkgbase_key) - signoff_groups = [] - for group in grouped: - signoff_group = PackageSignoffGroup(group, user=request.user) - signoff_group.target_repo = pkgtorepo.get(signoff_group.pkgbase, - "Unknown") - signoff_group.find_signoffs(signoffs) - signoff_groups.append(signoff_group) - - signoff_groups.sort(key=attrgetter('pkgbase')) + signoff_groups = sorted(get_signoff_groups(), key=attrgetter('pkgbase')) + for group in signoff_groups: + group.user = request.user context = { 'signoff_groups': signoff_groups, diff --git a/templates/packages/signoff_cell.html b/templates/packages/signoff_cell.html index fce5d551..87216193 100644 --- a/templates/packages/signoff_cell.html +++ b/templates/packages/signoff_cell.html @@ -5,8 +5,8 @@ {% if group.user_signed_off %} + title="Revoke signoff {{ group.pkgbase }} for {{ group.arch }}">Revoke Signoff {% else %} + title="Signoff {{ group.pkgbase }} for {{ group.arch }}">Signoff {% endif %} diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index 4a2f6c99..8d57a8c5 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -34,6 +34,7 @@

      Filter Displayed Signoffs

      + @@ -42,18 +43,17 @@

      Filter Displayed Signoffs

      {% for group in signoff_groups %} - {% with group.package as pkg %} - - - + + + + - + - {% endwith %} {% endfor %}
      Package Base/Version ArchPackage BaseTarget Repo # of PackagesVersion Last UpdatedTarget Repo ApprovedSignoffSignoffs
      {% pkg_details_link pkg %} {{ pkg.full_version }} {{ pkg.arch.name }}{% pkg_details_link pkg %}{{ group.target_repo }} {{ group.packages|length }}{{ pkg.full_version }} {{ pkg.last_update|date }}{{ group.target_repo }} {{ group.approved|yesno|capfirst }} -
        -
      • Signoff -
      • - {% for signoff in group.signoffs %} -
      • - {{ signoff.user }}{% if signoff.revoked %} (revoked){% endif %}
      • - {% endfor %} -
      -
      {% include "packages/signoff_cell.html" %}
      Package Base/Version Arch Target RepoPackager # of Packages Last Updated Approved
      {% pkg_details_link pkg %} {{ pkg.full_version }}{{ pkg.arch.name }}
      {% pkg_details_link group.package %} {{ group.version }}{{ group.arch.name }} {{ group.target_repo }}{{ group.packager|default:"Unknown" }} {{ group.packages|length }}{{ pkg.last_update|date }}{{ group.last_update|date }} {{ group.approved|yesno|capfirst }} {% include "packages/signoff_cell.html" %}
      -- cgit v1.2.3-54-g00ecf From 49ac7efd683152e4936f8013bb7a001470260034 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 17:18:55 -0500 Subject: Package signoff email report, initial revision Signed-off-by: Dan McGee --- packages/management/__init__.py | 0 packages/management/commands/__init__.py | 0 packages/management/commands/signoff_report.py | 110 +++++++++++++++++++++++++ templates/packages/signoff_report.txt | 27 ++++++ 4 files changed, 137 insertions(+) create mode 100644 packages/management/__init__.py create mode 100644 packages/management/commands/__init__.py create mode 100644 packages/management/commands/signoff_report.py create mode 100644 templates/packages/signoff_report.txt (limited to 'packages') diff --git a/packages/management/__init__.py b/packages/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/management/commands/__init__.py b/packages/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/management/commands/signoff_report.py b/packages/management/commands/signoff_report.py new file mode 100644 index 00000000..17e58f39 --- /dev/null +++ b/packages/management/commands/signoff_report.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +""" +signoff_report command + +Send an email summarizing the state of outstanding signoffs for the given +repository. + +Usage: ./manage.py signoff_report +""" + +from django.core.urlresolvers import reverse +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.db.models import Count +from django.template import loader, Context + +from collections import namedtuple +from datetime import datetime, timedelta +import logging +from operator import attrgetter +import sys + +from main.models import Package, Repo +from packages.models import Signoff +from packages.utils import get_signoff_groups + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s -> %(levelname)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + stream=sys.stderr) +logger = logging.getLogger() + +class Command(BaseCommand): + args = " " + help = "Send a signoff report for the given repository." + + def handle(self, *args, **options): + v = int(options.get('verbosity', None)) + if v == 0: + logger.level = logging.ERROR + elif v == 1: + logger.level = logging.INFO + elif v == 2: + logger.level = logging.DEBUG + + if len(args) != 2: + raise CommandError("email and repository must be provided") + + return generate_report(args[0], args[1]) + +def generate_report(email, repo_name): + repo = Repo.objects.get(name__iexact=repo_name) + # Collect all existing signoffs for these packages + signoff_groups = sorted(get_signoff_groups([repo]), + key=attrgetter('target_repo', 'arch', 'pkgbase')) + complete = [] + incomplete = [] + new = [] + old = [] + + new_hours = 24 + old_days = 14 + now = datetime.utcnow() + new_cutoff = now - timedelta(hours=new_hours) + old_cutoff = now - timedelta(days=old_days) + + for group in signoff_groups: + if group.approved(): + complete.append(group) + else: + incomplete.append(group) + if group.package.last_update > new_cutoff: + new.append(group) + if group.package.last_update < old_cutoff: + old.append(group) + + old.sort(key=attrgetter('last_update')) + + proto = 'https' + domain = Site.objects.get_current().domain + signoffs_url = '%s://%s%s' % (proto, domain, reverse('package-signoffs')) + + # and the fun bit + Leader = namedtuple('Leader', ['user', 'count']) + leaders = Signoff.objects.filter(created__gt=new_cutoff, + revoked__isnull=True).values_list('user').annotate( + signoff_count=Count('pk')).order_by('-signoff_count')[:5] + users = User.objects.in_bulk([l[0] for l in leaders]) + leaders = (Leader(users[l[0]], l[1]) for l in leaders) + + subject = 'Signoff report for [%s]' % repo.name.lower() + t = loader.get_template('packages/signoff_report.txt') + c = Context({ + 'repo': repo, + 'signoffs_url': signoffs_url, + 'incomplete': incomplete, + 'complete': complete, + 'new': new, + 'new_hours': new_hours, + 'old': old, + 'old_days': old_days, + 'leaders': leaders, + }) + from_addr = 'Arch Website Notification ' + #send_mail(subject, t.render(c), from_addr, email) + print t.render(c) + +# vim: set ts=4 sw=4 et: diff --git a/templates/packages/signoff_report.txt b/templates/packages/signoff_report.txt new file mode 100644 index 00000000..84e3fc6b --- /dev/null +++ b/templates/packages/signoff_report.txt @@ -0,0 +1,27 @@ +=== {% autoescape off %}Signoff report for [{{ repo|lower }}] === +{{ signoffs_url }} + +== New packages in [{{ repo|lower}}] in last {{ new_hours }} hours ({{ new|length }} total) == +{% for group in new %} +* {{ group.pkgbase }}-{{ group.version }} ({{ group.arch }}){% endfor %} + +{% regroup incomplete by target_repo as by_repo %}{% for target_repo in by_repo %} +== Incomplete signoffs for [{{ target_repo.grouper|lower }}] ({{ target_repo.list|length }} total) == +{% for group in target_repo.list %} +* {{ group.pkgbase }}-{{ group.version }} ({{ group.arch }}) + {{ group.completed }}/{{ group.required }} signoffs{% endfor %} +{% endfor %} + +== Completed signoffs ({{ complete|length }} total) == +{% for group in complete %} +* {{ group.pkgbase }}-{{ group.version }} ({{ group.arch }}){% endfor %} + + +== All packages in [{{ repo|lower }}] for more than {{ old_days }} days ({{ old|length }} total) == +{% for group in old %} +* {{ group.pkgbase }}-{{ group.version }} ({{ group.arch }}), since {{ group.last_update|date }}{% endfor %} +{% endautoescape %} + +== Top five in signoffs in last {{ new_hours }} hours == +{% for leader in leaders %} +{{ forloop.counter }}. {{ leader.user }} - {{ leader.count }} signoffs{% endfor %} -- cgit v1.2.3-54-g00ecf From 9a5410ba4b622b68306de53abfa28b5a49e30107 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 17:33:00 -0500 Subject: Make signoff_report command send email Signed-off-by: Dan McGee --- packages/management/commands/signoff_report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'packages') diff --git a/packages/management/commands/signoff_report.py b/packages/management/commands/signoff_report.py index 17e58f39..02f3d985 100644 --- a/packages/management/commands/signoff_report.py +++ b/packages/management/commands/signoff_report.py @@ -8,6 +8,7 @@ Usage: ./manage.py signoff_report """ +from django.core.mail import send_mail from django.core.urlresolvers import reverse from django.core.management.base import BaseCommand, CommandError from django.contrib.auth.models import User @@ -104,7 +105,6 @@ def generate_report(email, repo_name): 'leaders': leaders, }) from_addr = 'Arch Website Notification ' - #send_mail(subject, t.render(c), from_addr, email) - print t.render(c) + send_mail(subject, t.render(c), from_addr, [email]) # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 8187b87143081a2be75032db91287f9deb9d1f89 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 19:10:07 -0500 Subject: Add signoff options form and data entry page This allows the criteria and other information about certain signoffs to be overridden as necessary. Signed-off-by: Dan McGee --- packages/models.py | 52 ++++++++++++++++++++++++++------- packages/urls.py | 1 + packages/views.py | 40 ++++++++++++++++++++++++- templates/packages/signoff_cell.html | 15 ++++++++-- templates/packages/signoff_options.html | 18 ++++++++++++ templates/packages/signoffs.html | 3 +- 6 files changed, 113 insertions(+), 16 deletions(-) create mode 100644 templates/packages/signoff_options.html (limited to 'packages') diff --git a/packages/models.py b/packages/models.py index 3c319fe7..a2b53a06 100644 --- a/packages/models.py +++ b/packages/models.py @@ -38,6 +38,22 @@ def __unicode__(self): class Meta: unique_together = (('pkgbase', 'user', 'type'),) + +class SignoffSpecificationManager(models.Manager): + def get_from_package(self, pkg): + '''Utility method to pull all relevant name-version fields from a + package and get a matching specification.''' + return self.get( + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo) + + def get_or_create_from_package(self, pkg): + '''Utility method to pull all relevant name-version fields from a + package and get or create a matching specification.''' + return self.get_or_create( + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo) + class SignoffSpecification(models.Model): ''' A specification for the signoff policy for this particular revision of a @@ -53,25 +69,40 @@ class SignoffSpecification(models.Model): repo = models.ForeignKey('main.Repo') user = models.ForeignKey(User) created = models.DateTimeField(editable=False) - required = models.PositiveIntegerField(default=2) - enabled = models.BooleanField(default=True) - known_bad = models.BooleanField(default=False) + required = models.PositiveIntegerField(default=2, + help_text="How many signoffs are required for this package?") + enabled = models.BooleanField(default=True, + help_text="Is this package eligible for signoffs?") + known_bad = models.BooleanField(default=False, + help_text="Is package is known to be broken in some way?") comments = models.TextField(null=True, blank=True) + objects = SignoffSpecificationManager() + + @property + def full_version(self): + if self.epoch > 0: + return u'%d:%s-%s' % (self.epoch, self.pkgver, self.pkgrel) + return u'%s-%s' % (self.pkgver, self.pkgrel) + + def __unicode__(self): + return u'%s-%s' % (self.pkgbase, self.full_version) + + class SignoffManager(models.Manager): def get_from_package(self, pkg, user, revoked=False): '''Utility method to pull all relevant name-version fields from a - package and create a matching signoff.''' + package and get a matching signoff.''' not_revoked = not revoked - return Signoff.objects.get( + return self.get( pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo, revoked__isnull=not_revoked, user=user) def get_or_create_from_package(self, pkg, user): '''Utility method to pull all relevant name-version fields from a - package and create a matching signoff.''' - return Signoff.objects.get_or_create( + package and get or create a matching signoff.''' + return self.get_or_create( pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo, revoked=None, user=user) @@ -196,9 +227,8 @@ def remove_inactive_maintainers(sender, instance, created, **kwargs): post_save.connect(remove_inactive_maintainers, sender=User, dispatch_uid="packages.models") -pre_save.connect(set_created_field, sender=PackageRelation, - dispatch_uid="packages.models") -pre_save.connect(set_created_field, sender=Signoff, - dispatch_uid="packages.models") +for sender in (PackageRelation, SignoffSpecification, Signoff): + pre_save.connect(set_created_field, sender=sender, + dispatch_uid="packages.models") # vim: set ts=4 sw=4 et: diff --git a/packages/urls.py b/packages/urls.py index 576e3279..4d391a3c 100644 --- a/packages/urls.py +++ b/packages/urls.py @@ -11,6 +11,7 @@ (r'^unflag/all/$', 'unflag_all'), (r'^signoff/$', 'signoff_package'), (r'^signoff/revoke/$', 'signoff_package', {'revoke': True}), + (r'^signoff/options/$', 'signoff_options'), (r'^download/$', 'download'), ) diff --git a/packages/views.py b/packages/views.py index e102760b..66bcd3fc 100644 --- a/packages/views.py +++ b/packages/views.py @@ -25,7 +25,7 @@ from main.models import Package, PackageFile, Arch, Repo from main.utils import make_choice from mirrors.models import MirrorUrl -from .models import PackageRelation, PackageGroup, Signoff +from .models import PackageRelation, PackageGroup, SignoffSpecification, Signoff from .utils import (get_group_info, get_differences_info, get_wrong_permissions, get_signoff_groups, approved_by_signoffs) @@ -417,6 +417,44 @@ def signoff_package(request, name, repo, arch, revoke=False): return redirect('package-signoffs') +class SignoffOptionsForm(forms.ModelForm): + class Meta: + model = SignoffSpecification + fields = ('required', 'enabled', 'known_bad', 'comments') + +@permission_required('main.change_package') +@never_cache +def signoff_options(request, name, repo, arch): + packages = get_list_or_404(Package, pkgbase=name, + arch__name=arch, repo__name__iexact=repo, repo__testing=True) + package = packages[0] + + # TODO ensure submitter is maintainer and/or packager + + try: + spec = SignoffSpecification.objects.get_from_package(package) + except SignoffSpecification.DoesNotExist: + # create a fake one, but don't save it just yet + spec = SignoffSpecification(pkgbase=package.pkgbase, + pkgver=package.pkgver, pkgrel=package.pkgrel, + epoch=package.epoch, arch=package.arch, repo=package.repo) + spec.user = request.user + + if request.POST: + form = SignoffOptionsForm(request.POST, instance=spec) + if form.is_valid(): + form.save() + return redirect('package-signoffs') + else: + form = SignoffOptionsForm(instance=spec) + + context = { + 'packages': packages, + 'package': package, + 'form': form, + } + return direct_to_template(request, 'packages/signoff_options.html', context) + def flaghelp(request): return direct_to_template(request, 'packages/flaghelp.html') diff --git a/templates/packages/signoff_cell.html b/templates/packages/signoff_cell.html index 87216193..0a630119 100644 --- a/templates/packages/signoff_cell.html +++ b/templates/packages/signoff_cell.html @@ -1,12 +1,23 @@ +{% spaceless %} +{% if group.signoffs %}
        {% for signoff in group.signoffs %}
      • {{ signoff.user }}{% if signoff.revoked %} (revoked){% endif %}
      • {% endfor %}
      +{% endif %} {% if group.user_signed_off %} - {% else %} - {% endif %} +{% if group.packager == user %} + +{% endif %} +{% endspaceless %} diff --git a/templates/packages/signoff_options.html b/templates/packages/signoff_options.html new file mode 100644 index 00000000..ee9b8b47 --- /dev/null +++ b/templates/packages/signoff_options.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block title %}Arch Linux - Package Signoff Options - {{ package.pkgbase }} {{ package.full_version }} ({{ package.arch.name }}){% endblock %} +{% block head %}{% endblock %} +{% block navbarclass %}anb-packages{% endblock %} + +{% block content %} +
      +

      Package Signoff Options: {{ package.pkgbase }} {{ package.full_version }} ({{ package.arch.name }})

      +
      {% csrf_token %} +
      + {{ form.as_p }} +
      +

      +
      + +
      +{% endblock %} diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index 0bdc6d46..9bc7fd74 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -50,8 +50,7 @@

      Filter Displayed Signoffs

      {{ group.packager|default:"Unknown" }} {{ group.packages|length }} {{ group.last_update|date }} - - {{ group.approved|yesno|capfirst }} + {{ group.approved|yesno|capfirst }} {% include "packages/signoff_cell.html" %} {% endfor %} -- cgit v1.2.3-54-g00ecf From 5f2c3bf98baabf919681525e600639643aa2c119 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 20:39:59 -0500 Subject: Signoffs changes and improvements * Better signoff report with more detail * Show signoff specification in signoffs view * Honor disabled/bad flags and display in approval column * Various other small bugfixes and tweaks Signed-off-by: Dan McGee --- media/archweb.css | 8 +++++-- media/archweb.js | 30 ++++++++++++++++---------- packages/management/commands/signoff_report.py | 13 ++++++++++- packages/models.py | 28 +++++++++++++++++------- packages/utils.py | 16 ++++++++------ packages/views.py | 15 +++++++++---- templates/packages/signoff_cell.html | 2 ++ templates/packages/signoff_report.txt | 13 +++++++++++ templates/packages/signoffs.html | 19 ++++++++++++++-- 9 files changed, 109 insertions(+), 35 deletions(-) (limited to 'packages') diff --git a/media/archweb.css b/media/archweb.css index f817e18d..303173f2 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -914,8 +914,12 @@ ul.admin-actions { color: red; } -#dev-signoffs .signed-username { - color: #888; +#dev-signoffs .signoff-bad { + color: darkorange; +} + +#dev-signoffs .signoff-disabled { + color: gray; } /* iso testing feedback form */ diff --git a/media/archweb.js b/media/archweb.js index 43812b33..a9f4e0c9 100644 --- a/media/archweb.js +++ b/media/archweb.js @@ -215,28 +215,33 @@ function signoff_package() { $.getJSON(link.href, function(data) { link = $(link); var signoff = null; + var cell = link.closest('td'); if (data.created) { signoff = $('
    • ').addClass('signed-username').text(data.user); - link.closest('td').children('ul').append(signoff); + var list = cell.children('ul'); + if (list.size() == 0) { + list = $('
        ').prependTo(cell); + } + list.append(signoff); } else if(data.user) { signoff = link.closest('td').find('li').filter(function(index) { return $(this).text() == data.user; }); } - console.log(signoff, data.revoked, data.user); if (signoff && data.revoked) { signoff.text(signoff.text() + ' (revoked)'); } /* update the approved column to reflect reality */ - var approved; - if (data.approved) { - approved = link.closest('tr').children('.signoff-no'); - approved.text('Yes').addClass( - 'signoff-yes').removeClass('signoff-no'); + var approved = link.closest('tr').children('.approval'); + approved.attr('class', ''); + if (data.known_bad) { + approved.text('Bad').addClass('signoff-bad'); + } else if (!data.enabled) { + approved.text('Disabled').addClass('signoff-disabled'); + } else if (data.approved) { + approved.text('Yes').addClass('signoff-yes'); } else { - approved = link.closest('tr').children('.signoff-yes'); - approved.text('No').addClass( - 'signoff-no').removeClass('signoff-yes'); + approved.text('No').addClass('signoff-no'); } link.removeAttr('title'); /* Form our new link. The current will be something like @@ -245,6 +250,10 @@ function signoff_package() { if (data.revoked) { link.text('Signoff'); link.attr('href', base_href + '/signoff/'); + /* should we be hiding the link? */ + if (data.known_bad || !data.enabled) { + link.remove(); + } } else { link.text('Revoke Signoff'); link.attr('href', base_href + '/signoff/revoke/'); @@ -260,7 +269,6 @@ function filter_signoffs() { var all_rows = rows; $('#signoffs_filter .arch_filter').each(function() { if (!$(this).is(':checked')) { - console.log($(this).val()); rows = rows.not('.' + $(this).val()); } }); diff --git a/packages/management/commands/signoff_report.py b/packages/management/commands/signoff_report.py index 02f3d985..3431dada 100644 --- a/packages/management/commands/signoff_report.py +++ b/packages/management/commands/signoff_report.py @@ -56,6 +56,8 @@ def generate_report(email, repo_name): # Collect all existing signoffs for these packages signoff_groups = sorted(get_signoff_groups([repo]), key=attrgetter('target_repo', 'arch', 'pkgbase')) + disabled = [] + bad = [] complete = [] incomplete = [] new = [] @@ -68,10 +70,16 @@ def generate_report(email, repo_name): old_cutoff = now - timedelta(days=old_days) for group in signoff_groups: - if group.approved(): + spec = group.specification + if spec.known_bad: + bad.append(group) + elif not spec.enabled: + disabled.append(group) + elif group.approved(): complete.append(group) else: incomplete.append(group) + if group.package.last_update > new_cutoff: new.append(group) if group.package.last_update < old_cutoff: @@ -96,6 +104,9 @@ def generate_report(email, repo_name): c = Context({ 'repo': repo, 'signoffs_url': signoffs_url, + 'disabled': disabled, + 'bad': bad, + 'all': signoff_groups, 'incomplete': incomplete, 'complete': complete, 'new': new, diff --git a/packages/models.py b/packages/models.py index a2b53a06..b70c21bf 100644 --- a/packages/models.py +++ b/packages/models.py @@ -1,3 +1,5 @@ +from collections import namedtuple + from django.db import models from django.db.models.signals import pre_save, post_save from django.contrib.auth.models import User @@ -42,22 +44,26 @@ class Meta: class SignoffSpecificationManager(models.Manager): def get_from_package(self, pkg): '''Utility method to pull all relevant name-version fields from a - package and get a matching specification.''' + package and get a matching signoff specification.''' return self.get( pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo) - def get_or_create_from_package(self, pkg): - '''Utility method to pull all relevant name-version fields from a - package and get or create a matching specification.''' - return self.get_or_create( - pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, - epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo) + def get_or_default_from_package(self, pkg): + '''utility method to pull all relevant name-version fields from a + package and get a matching signoff specification, or return the default + base case.''' + try: + return self.get( + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo) + except SignoffSpecification.DoesNotExist: + return DEFAULT_SIGNOFF_SPEC class SignoffSpecification(models.Model): ''' A specification for the signoff policy for this particular revision of a - pakcage. The default is requiring two signoffs for a given package. These + package. The default is requiring two signoffs for a given package. These are created only if necessary; e.g., if one wanted to override the required=2 attribute, otherwise a sane default object is used. ''' @@ -89,6 +95,12 @@ def __unicode__(self): return u'%s-%s' % (self.pkgbase, self.full_version) +# fake default signoff spec when we don't have a persisted one in the database +FakeSignoffSpecification = namedtuple('FakeSignoffSpecification', + ('required', 'enabled', 'known_bad', 'comments')) +DEFAULT_SIGNOFF_SPEC = FakeSignoffSpecification(2, True, False, u'') + + class SignoffManager(models.Manager): def get_from_package(self, pkg, user, revoked=False): '''Utility method to pull all relevant name-version fields from a diff --git a/packages/utils.py b/packages/utils.py index 42cfbe0f..60b95e21 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -5,7 +5,8 @@ from main.models import Package, Repo from main.utils import cache_function, groupby_preserve_order, PackageStandin -from .models import PackageGroup, PackageRelation, SignoffSpecification, Signoff +from .models import (PackageGroup, PackageRelation, + SignoffSpecification, Signoff, DEFAULT_SIGNOFF_SPEC) @cache_function(300) def get_group_info(include_arches=None): @@ -148,9 +149,7 @@ def get_wrong_permissions(): return relations -DEFAULT_SIGNOFF_SPEC = SignoffSpecification() - -def approved_by_signoffs(signoffs, spec=DEFAULT_SIGNOFF_SPEC): +def approved_by_signoffs(signoffs, spec): if signoffs: good_signoffs = sum(1 for s in signoffs if not s.revoked) return good_signoffs >= spec.required @@ -158,14 +157,13 @@ def approved_by_signoffs(signoffs, spec=DEFAULT_SIGNOFF_SPEC): class PackageSignoffGroup(object): '''Encompasses all packages in testing with the same pkgbase.''' - def __init__(self, packages, user=None): + def __init__(self, packages): if len(packages) == 0: raise Exception self.packages = packages - self.user = user + self.user = None self.target_repo = None self.signoffs = set() - self.specification = DEFAULT_SIGNOFF_SPEC first = packages[0] self.pkgbase = first.pkgbase @@ -175,6 +173,10 @@ def __init__(self, packages, user=None): self.last_update = first.last_update self.packager = first.packager + self.specification = \ + SignoffSpecification.objects.get_or_default_from_package(first) + self.default_spec = self.specification is DEFAULT_SIGNOFF_SPEC + version = first.full_version if all(version == pkg.full_version for pkg in packages): self.version = version diff --git a/packages/views.py b/packages/views.py index 66bcd3fc..307691e2 100644 --- a/packages/views.py +++ b/packages/views.py @@ -7,8 +7,9 @@ from django.core.mail import send_mail from django.core.serializers.json import DjangoJSONEncoder from django.db.models import Q -from django.http import HttpResponse, Http404 -from django.shortcuts import get_object_or_404, get_list_or_404, redirect +from django.http import HttpResponse, Http404, HttpResponseForbidden +from django.shortcuts import (get_object_or_404, get_list_or_404, + redirect, render) from django.template import loader, Context from django.utils import simplejson from django.views.decorators.cache import never_cache @@ -404,12 +405,16 @@ def signoff_package(request, name, repo, arch, revoke=False): package, request.user) all_signoffs = Signoff.objects.for_package(package) + spec = SignoffSpecification.objects.get_or_default_from_package(package) if request.is_ajax(): data = { 'created': created, 'revoked': bool(signoff.revoked), - 'approved': approved_by_signoffs(all_signoffs), + 'approved': approved_by_signoffs(all_signoffs, spec), + 'required': spec.required, + 'enabled': spec.enabled, + 'known_bad': spec.known_bad, 'user': str(request.user), } return HttpResponse(simplejson.dumps(data, ensure_ascii=False), @@ -429,7 +434,9 @@ def signoff_options(request, name, repo, arch): arch__name=arch, repo__name__iexact=repo, repo__testing=True) package = packages[0] - # TODO ensure submitter is maintainer and/or packager + if request.user != package.packager and \ + request.user not in package.maintainers: + return render(request, '403.html', status=403) try: spec = SignoffSpecification.objects.get_from_package(package) diff --git a/templates/packages/signoff_cell.html b/templates/packages/signoff_cell.html index 0a630119..6c705b4e 100644 --- a/templates/packages/signoff_cell.html +++ b/templates/packages/signoff_cell.html @@ -11,10 +11,12 @@ Revoke Signoff
    {% else %} +{% if not group.specification.known_bad and group.specification.enabled %} {% endif %} +{% endif %} {% if group.packager == user %}
    Packager Options diff --git a/templates/packages/signoff_report.txt b/templates/packages/signoff_report.txt index 84e3fc6b..81020c8f 100644 --- a/templates/packages/signoff_report.txt +++ b/templates/packages/signoff_report.txt @@ -1,6 +1,19 @@ === {% autoescape off %}Signoff report for [{{ repo|lower }}] === {{ signoffs_url }} +There are currently: +* {{ new|length }} new package{{ new|length|pluralize }} in last {{ new_hours }} hours +* {{ bad|length }} known bad package{{ bad|length|pluralize }} +* {{ disabled|length }} package{{ disabled|length|pluralize }} not accepting signoffs +* {{ complete|length }} fully signed off package{{ complete|length|pluralize }} +* {{ incomplete|length }} package{{ incomplete|length|pluralize }} missing signoffs +* {{ old|length }} package{{ old|length|pluralize }} older than {{ old_days }} days + +(Note: the word 'package' as used here refers to packages as grouped by +pkgbase, architecture, and repository; e.g., one PKGBUILD produces one +package per architecture, even if it is a split package.) + + == New packages in [{{ repo|lower}}] in last {{ new_hours }} hours ({{ new|length }} total) == {% for group in new %} * {{ group.pkgbase }}-{{ group.version }} ({{ group.arch }}){% endfor %} diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index 9bc7fd74..d517e5e3 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -39,6 +39,7 @@

    Filter Displayed Signoffs

    Last Updated Approved Signoffs + Notes @@ -50,8 +51,22 @@

    Filter Displayed Signoffs

    {{ group.packager|default:"Unknown" }} {{ group.packages|length }} {{ group.last_update|date }} - {{ group.approved|yesno|capfirst }} + {% if group.specification.known_bad %} + Bad + {% else %} + {% if not group.specification.enabled %} + Disabled + {% else %} + {{ group.approved|yesno|capfirst }} + {% endif %} + {% endif %} {% include "packages/signoff_cell.html" %} + {% if not group.default_spec %}{% with group.specification as spec %} + {% if spec.required != 2 %}Required signoffs: {{ spec.required }}
    {% endif %} + {% if not spec.enabled %}Signoffs are not currently enabled
    {% endif %} + {% if spec.known_bad %}Package is known to be bad
    {% endif %} + {{ spec.comments|default:""|linebreaks }} + {% endwith %}{% endif %} {% endfor %} @@ -64,7 +79,7 @@

    Filter Displayed Signoffs

    $(document).ready(function() { $('a.signoff-link').click(signoff_package); $(".results").tablesorter({widgets: ['zebra'], sortList: [[0,0]], - headers: { 7: { sorter: false } } }); + headers: { 7: { sorter: false }, 8: {sorter: false } } }); $('#signoffs_filter input').change(filter_signoffs); $('#criteria_reset').click(filter_signoffs_reset); // fire function on page load to ensure the current form selections take effect -- cgit v1.2.3-54-g00ecf From 800ea45528e297c38e068775951e666f8191ef45 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 21:20:28 -0500 Subject: Ensure signoffs can only be created if allowed Signed-off-by: Dan McGee --- packages/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'packages') diff --git a/packages/views.py b/packages/views.py index 307691e2..00dd7f7d 100644 --- a/packages/views.py +++ b/packages/views.py @@ -388,9 +388,10 @@ def signoffs(request): def signoff_package(request, name, repo, arch, revoke=False): packages = get_list_or_404(Package, pkgbase=name, arch__name=arch, repo__name__iexact=repo, repo__testing=True) - package = packages[0] + spec = SignoffSpecification.objects.get_or_default_from_package(package) + if revoke: try: signoff = Signoff.objects.get_from_package( @@ -401,11 +402,13 @@ def signoff_package(request, name, repo, arch, revoke=False): signoff.save() created = False else: + # ensure we should even be accepting signoffs + if spec.known_bad or not spec.enabled: + return render(request, '403.html', status=403) signoff, created = Signoff.objects.get_or_create_from_package( package, request.user) all_signoffs = Signoff.objects.for_package(package) - spec = SignoffSpecification.objects.get_or_default_from_package(package) if request.is_ajax(): data = { -- cgit v1.2.3-54-g00ecf From 5e295a3dbb0b64f229e9419384721b154e013b9e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 21:20:50 -0500 Subject: Allow signoff options to apply to all packages across architectures If you check the new box, you can set the options for both the i686 and the x86_64 packages at the same time. Signed-off-by: Dan McGee --- packages/views.py | 32 +++++++++++++++++++++++++++++++- templates/packages/signoff_cell.html | 2 +- 2 files changed, 32 insertions(+), 2 deletions(-) (limited to 'packages') diff --git a/packages/views.py b/packages/views.py index 00dd7f7d..aa15d0cf 100644 --- a/packages/views.py +++ b/packages/views.py @@ -6,6 +6,7 @@ from django.conf import settings from django.core.mail import send_mail from django.core.serializers.json import DjangoJSONEncoder +from django.db import transaction from django.db.models import Q from django.http import HttpResponse, Http404, HttpResponseForbidden from django.shortcuts import (get_object_or_404, get_list_or_404, @@ -426,10 +427,36 @@ def signoff_package(request, name, repo, arch, revoke=False): return redirect('package-signoffs') class SignoffOptionsForm(forms.ModelForm): + apply_all = forms.BooleanField(required=False, + help_text="Apply these options to all architectures?") + class Meta: model = SignoffSpecification fields = ('required', 'enabled', 'known_bad', 'comments') +def _signoff_options_all(request, name, repo): + seen_ids = set() + with transaction.commit_on_success(): + # find or create a specification for all architectures, then + # graft the form data onto them + packages = Package.objects.filter(pkgbase=name, + repo__name__iexact=repo, repo__testing=True) + for package in packages: + try: + spec = SignoffSpecification.objects.get_from_package(package) + if spec.pk in seen_ids: + continue + except SignoffSpecification.DoesNotExist: + spec = SignoffSpecification(pkgbase=package.pkgbase, + pkgver=package.pkgver, pkgrel=package.pkgrel, + epoch=package.epoch, arch=package.arch, + repo=package.repo) + spec.user = request.user + form = SignoffOptionsForm(request.POST, instance=spec) + if form.is_valid(): + form.save() + seen_ids.add(form.instance.pk) + @permission_required('main.change_package') @never_cache def signoff_options(request, name, repo, arch): @@ -453,7 +480,10 @@ def signoff_options(request, name, repo, arch): if request.POST: form = SignoffOptionsForm(request.POST, instance=spec) if form.is_valid(): - form.save() + if form.cleaned_data['apply_all']: + _signoff_options_all(request, name, repo) + else: + form.save() return redirect('package-signoffs') else: form = SignoffOptionsForm(instance=spec) diff --git a/templates/packages/signoff_cell.html b/templates/packages/signoff_cell.html index 6c705b4e..4f9f726b 100644 --- a/templates/packages/signoff_cell.html +++ b/templates/packages/signoff_cell.html @@ -19,7 +19,7 @@ {% endif %} {% if group.packager == user %} {% endif %} {% endspaceless %} -- cgit v1.2.3-54-g00ecf From 0aa42e2c01df2bf1c9e425994420f5ae10252597 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 21:32:30 -0500 Subject: Allow signoff manipulation if you are a maintainer This is a more expensive and not-yet-optimized way of doing this, but we can fix that later as needed. Signed-off-by: Dan McGee --- packages/utils.py | 4 ++++ templates/packages/signoff_cell.html | 2 +- templates/todolists/view.html | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) (limited to 'packages') diff --git a/packages/utils.py b/packages/utils.py index 60b95e21..1a2c0de0 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -2,6 +2,7 @@ from django.db import connection from django.db.models import Count, Max +from django.contrib.auth.models import User from main.models import Package, Repo from main.utils import cache_function, groupby_preserve_order, PackageStandin @@ -172,6 +173,9 @@ def __init__(self, packages): self.version = '' self.last_update = first.last_update self.packager = first.packager + self.maintainers = User.objects.filter( + package_relations__type=PackageRelation.MAINTAINER, + package_relations__pkgbase=self.pkgbase) self.specification = \ SignoffSpecification.objects.get_or_default_from_package(first) diff --git a/templates/packages/signoff_cell.html b/templates/packages/signoff_cell.html index 4f9f726b..0bf44ca2 100644 --- a/templates/packages/signoff_cell.html +++ b/templates/packages/signoff_cell.html @@ -17,7 +17,7 @@ title="Signoff {{ group.pkgbase }} for {{ group.arch }}">Signoff
    {% endif %} {% endif %} -{% if group.packager == user %} +{% if user == group.packager or user in group.maintainers %} diff --git a/templates/todolists/view.html b/templates/todolists/view.html index 8f515c9b..c9ea919a 100644 --- a/templates/todolists/view.html +++ b/templates/todolists/view.html @@ -29,7 +29,7 @@

    Todo List: {{ list.name }}

    Name Arch Repo - Maintainer + Maintainers Status -- cgit v1.2.3-54-g00ecf From 278d74b1d12568d4c9b6d5533e57e820d038ae64 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 22:05:35 -0500 Subject: Minor signoff query tweaks/optimizations Signed-off-by: Dan McGee --- packages/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'packages') diff --git a/packages/utils.py b/packages/utils.py index 1a2c0de0..65769baf 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -248,8 +248,7 @@ def get_current_signoffs(repos): AND s.arch_id = p.arch_id AND s.repo_id = p.repo_id ) - JOIN repos r ON p.repo_id = r.id - WHERE r.id IN ( + WHERE p.repo_id IN ( """ sql += ", ".join("%s" for r in repos) sql += ")" @@ -264,15 +263,16 @@ def get_current_signoffs(repos): def get_target_repo_map(pkgbases): package_repos = Package.objects.order_by().values_list( 'pkgbase', 'repo__name').filter( - repo__testing=False, repo__staging=False, pkgbase__in=pkgbases).distinct() return dict(package_repos) def get_signoff_groups(repos=None): if repos is None: repos = Repo.objects.filter(testing=True) + repo_ids = [r.pk for r in repos] - test_pkgs = Package.objects.normal().filter(repo__in=repos) + test_pkgs = Package.objects.select_related( + 'arch', 'repo', 'packager').filter(repo__in=repo_ids) packages = test_pkgs.order_by('pkgname') # Collect all pkgbase values in testing repos -- cgit v1.2.3-54-g00ecf From 19c2841f20653fd3c59f73fdb16f7f7b1ea15434 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 22:46:36 -0500 Subject: Add new attach_maintainers() utility method This allows us to alleviate the N+1 query problem when we want maintainer data for a queryset of packages. We use it on signoffs here; we should also be able to apply this to the todolist section where this problem has existed for some time. Signed-off-by: Dan McGee --- main/models.py | 14 +++++++++++--- packages/utils.py | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) (limited to 'packages') diff --git a/main/models.py b/main/models.py index 972b098c..db456c20 100644 --- a/main/models.py +++ b/main/models.py @@ -206,11 +206,19 @@ def get_full_url(self, proto='https'): def is_signed(self): return bool(self.pgp_signature) + _maintainers = None + @property def maintainers(self): - return User.objects.filter( - package_relations__pkgbase=self.pkgbase, - package_relations__type=PackageRelation.MAINTAINER) + if self._maintainers is None: + self._maintainers = User.objects.filter( + package_relations__pkgbase=self.pkgbase, + package_relations__type=PackageRelation.MAINTAINER) + return self._maintainers + + @maintainers.setter + def maintainers(self, maintainers): + self._maintainers = maintainers @cache_function(300) def applicable_arches(self): diff --git a/packages/utils.py b/packages/utils.py index 65769baf..0d756a85 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -1,3 +1,4 @@ +from collections import defaultdict from operator import itemgetter from django.db import connection @@ -127,6 +128,7 @@ def get_differences_info(arch_a, arch_b): differences.sort(key=lambda a: (a.repo.name, a.pkgname)) return differences + def get_wrong_permissions(): sql = """ SELECT DISTINCT id @@ -150,6 +152,32 @@ def get_wrong_permissions(): return relations +def attach_maintainers(packages): + '''Given a queryset or something resembling it of package objects, find all + the maintainers and attach them to the packages to prevent N+1 query + cascading.''' + pkgbases = set(p.pkgbase for p in packages) + rels = PackageRelation.objects.filter(type=PackageRelation.MAINTAINER, + pkgbase__in=pkgbases).values_list('pkgbase', 'user_id').distinct() + + # get all the user objects we will need + user_ids = set(rel[1] for rel in rels) + users = User.objects.in_bulk(user_ids) + + # now build a pkgbase -> [maintainers...] map + maintainers = defaultdict(list) + for rel in rels: + maintainers[rel[0]].append(users[rel[1]]) + + annotated = [] + # and finally, attach the maintainer lists on the original packages + for package in packages: + package.maintainers = maintainers[package.pkgbase] + annotated.append(package) + + return annotated + + def approved_by_signoffs(signoffs, spec): if signoffs: good_signoffs = sum(1 for s in signoffs if not s.revoked) @@ -173,9 +201,7 @@ def __init__(self, packages): self.version = '' self.last_update = first.last_update self.packager = first.packager - self.maintainers = User.objects.filter( - package_relations__type=PackageRelation.MAINTAINER, - package_relations__pkgbase=self.pkgbase) + self.maintainers = first.maintainers self.specification = \ SignoffSpecification.objects.get_or_default_from_package(first) @@ -236,6 +262,7 @@ def __unicode__(self): def get_current_signoffs(repos): '''Returns a mapping of pkgbase -> signoff objects for the given repos.''' + # TODO this isn't current at all- this is every single signoff... cursor = connection.cursor() sql = """ SELECT DISTINCT s.id @@ -274,6 +301,7 @@ def get_signoff_groups(repos=None): test_pkgs = Package.objects.select_related( 'arch', 'repo', 'packager').filter(repo__in=repo_ids) packages = test_pkgs.order_by('pkgname') + packages = attach_maintainers(packages) # Collect all pkgbase values in testing repos q_pkgbase = test_pkgs.values('pkgbase') -- cgit v1.2.3-54-g00ecf From 0db2830b8fda4d898a184a31f3375c10f3cc4083 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 23:30:16 -0500 Subject: Make maintainer lookup on todo lists fast This is rather sick to look at. Sorry, Django gives me no other choice. Signed-off-by: Dan McGee --- main/models.py | 13 +++++++++---- packages/utils.py | 1 + todolists/views.py | 8 +++++++- 3 files changed, 17 insertions(+), 5 deletions(-) (limited to 'packages') diff --git a/main/models.py b/main/models.py index db456c20..caf36be0 100644 --- a/main/models.py +++ b/main/models.py @@ -460,12 +460,17 @@ class Todolist(models.Model): def __unicode__(self): return self.name + _packages = None + @property def packages(self): - # select_related() does not use LEFT OUTER JOIN for nullable ForeignKey - # fields. That is why we need to explicitly list the ones we want. - return TodolistPkg.objects.select_related( - 'pkg__repo', 'pkg__arch').filter(list=self).order_by('pkg') + if not self._packages: + # select_related() does not use LEFT OUTER JOIN for nullable + # ForeignKey fields. That is why we need to explicitly list the + # ones we want. + self._packages = TodolistPkg.objects.select_related( + 'pkg__repo', 'pkg__arch').filter(list=self).order_by('pkg') + return self._packages @property def package_names(self): diff --git a/packages/utils.py b/packages/utils.py index 0d756a85..4af0f67d 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -156,6 +156,7 @@ def attach_maintainers(packages): '''Given a queryset or something resembling it of package objects, find all the maintainers and attach them to the packages to prevent N+1 query cascading.''' + packages = list(packages) pkgbases = set(p.pkgbase for p in packages) rels = PackageRelation.objects.filter(type=PackageRelation.MAINTAINER, pkgbase__in=pkgbases).values_list('pkgbase', 'user_id').distinct() diff --git a/todolists/views.py b/todolists/views.py index 8ad7be56..585cefd0 100644 --- a/todolists/views.py +++ b/todolists/views.py @@ -12,6 +12,7 @@ from django.utils import simplejson from main.models import Todolist, TodolistPkg, Package +from packages.utils import attach_maintainers from .utils import get_annotated_todolists class TodoListForm(forms.ModelForm): @@ -49,6 +50,9 @@ def flag(request, listid, pkgid): @never_cache def view(request, listid): todolist = get_object_or_404(Todolist, id=listid) + # we don't hold onto the result, but the objects are the same here, + # so accessing maintainers in the template is now cheap + attach_maintainers(tp.pkg for tp in todolist.packages) return direct_to_template(request, 'todolists/view.html', {'list': todolist}) @login_required @@ -163,8 +167,10 @@ def send_todolist_emails(todo_list, new_packages): def public_list(request): todo_lists = Todolist.objects.incomplete() + # total hackjob, but it makes this a lot less query-intensive. + all_pkgs = [tp for tl in todo_lists for tp in tl.packages] + attach_maintainers([tp.pkg for tp in all_pkgs]) return direct_to_template(request, "todolists/public_list.html", {"todo_lists": todo_lists}) - # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 94f46acebf03652d7ad2ed504d4ce863d5cbd913 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 4 Nov 2011 00:01:51 -0500 Subject: Find all potential package signoff specifications upfront This should save a significant amount of time in the case where there are a lot of signups to look up; at least one query per signoff row. Signed-off-by: Dan McGee --- packages/utils.py | 54 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 15 deletions(-) (limited to 'packages') diff --git a/packages/utils.py b/packages/utils.py index 4af0f67d..5240ae23 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -194,6 +194,8 @@ def __init__(self, packages): self.user = None self.target_repo = None self.signoffs = set() + self.specification = DEFAULT_SIGNOFF_SPEC + self.default_spec = True first = packages[0] self.pkgbase = first.pkgbase @@ -204,10 +206,6 @@ def __init__(self, packages): self.packager = first.packager self.maintainers = first.maintainers - self.specification = \ - SignoffSpecification.objects.get_or_default_from_package(first) - self.default_spec = self.specification is DEFAULT_SIGNOFF_SPEC - version = first.full_version if all(version == pkg.full_version for pkg in packages): self.version = version @@ -238,6 +236,17 @@ def find_signoffs(self, all_signoffs): if s.arch_id == self.arch.id and s.repo_id == self.repo.id: self.signoffs.add(s) + def find_specification(self, specifications): + for spec in specifications: + if spec.pkgbase != self.pkgbase: + continue + if self.version and not spec.full_version == self.version: + continue + if spec.arch_id == self.arch.id and spec.repo_id == self.repo.id: + self.specification = spec + self.default_spec = False + return + def approved(self): return approved_by_signoffs(self.signoffs, self.specification) @@ -261,13 +270,9 @@ def __unicode__(self): return u'%s-%s (%s): %d' % ( self.pkgbase, self.version, self.arch, len(self.signoffs)) -def get_current_signoffs(repos): - '''Returns a mapping of pkgbase -> signoff objects for the given repos.''' - # TODO this isn't current at all- this is every single signoff... - cursor = connection.cursor() - sql = """ +_SQL_SPEC_OR_SIGNOFF = """ SELECT DISTINCT s.id - FROM packages_signoff s + FROM %s s JOIN packages p ON ( s.pkgbase = p.pkgbase AND s.pkgver = p.pkgver @@ -276,11 +281,16 @@ def get_current_signoffs(repos): AND s.arch_id = p.arch_id AND s.repo_id = p.repo_id ) - WHERE p.repo_id IN ( + AND p.repo_id IN (%s) """ - sql += ", ".join("%s" for r in repos) - sql += ")" - cursor.execute(sql, [r.id for r in repos]) + +def get_current_signoffs(repos): + '''Returns a mapping of pkgbase -> signoff objects for the given repos.''' + cursor = connection.cursor() + # query pre-process- fill in table name and placeholders for IN + sql = _SQL_SPEC_OR_SIGNOFF % ('packages_signoff', + ','.join(['%s' for r in repos])) + cursor.execute(sql, [r.pk for r in repos]) results = cursor.fetchall() # fetch all of the returned signoffs by ID @@ -288,6 +298,18 @@ def get_current_signoffs(repos): signoffs = Signoff.objects.select_related('user').in_bulk(to_fetch) return signoffs.values() +def get_current_specifications(repos): + '''Returns a mapping of pkgbase -> signoff specification objects for the + given repos.''' + cursor = connection.cursor() + sql = _SQL_SPEC_OR_SIGNOFF % ('packages_signoffspecification', + ','.join(['%s' for r in repos])) + cursor.execute(sql, [r.pk for r in repos]) + + results = cursor.fetchall() + to_fetch = [row[0] for row in results] + return SignoffSpecification.objects.in_bulk(to_fetch).values() + def get_target_repo_map(pkgbases): package_repos = Package.objects.order_by().values_list( 'pkgbase', 'repo__name').filter( @@ -308,8 +330,9 @@ def get_signoff_groups(repos=None): q_pkgbase = test_pkgs.values('pkgbase') pkgtorepo = get_target_repo_map(q_pkgbase) - # Collect all existing signoffs for these packages + # Collect all possible signoffs and specifications for these packages signoffs = get_current_signoffs(repos) + specs = get_current_specifications(repos) same_pkgbase_key = lambda x: (x.repo.name, x.arch.name, x.pkgbase) grouped = groupby_preserve_order(packages, same_pkgbase_key) @@ -319,6 +342,7 @@ def get_signoff_groups(repos=None): signoff_group.target_repo = pkgtorepo.get(signoff_group.pkgbase, "Unknown") signoff_group.find_signoffs(signoffs) + signoff_group.find_specification(specs) signoff_groups.append(signoff_group) return signoff_groups -- cgit v1.2.3-54-g00ecf From 20ebe658921c2ce78bf9a05116de045ee38f0820 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 4 Nov 2011 10:43:02 -0500 Subject: Fix signoff target repo mapping I clearly should not have removed this code yesterday, otherwise packages have their target repo matched to a testing one. Signed-off-by: Dan McGee --- packages/utils.py | 1 + 1 file changed, 1 insertion(+) (limited to 'packages') diff --git a/packages/utils.py b/packages/utils.py index 5240ae23..ddd822e4 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -313,6 +313,7 @@ def get_current_specifications(repos): def get_target_repo_map(pkgbases): package_repos = Package.objects.order_by().values_list( 'pkgbase', 'repo__name').filter( + repo__testing=False, repo__staging=False, pkgbase__in=pkgbases).distinct() return dict(package_repos) -- cgit v1.2.3-54-g00ecf From 8ba68aed370c2369bebaaca4d4158b6c40223c0f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 4 Nov 2011 10:59:45 -0500 Subject: Add filter by target repo on signoffs page And add a count of displayed rows below the filter options. Signed-off-by: Dan McGee --- media/archweb.js | 7 ++++++- packages/views.py | 1 + templates/packages/signoffs.html | 12 +++++++++--- 3 files changed, 16 insertions(+), 4 deletions(-) (limited to 'packages') diff --git a/media/archweb.js b/media/archweb.js index a9f4e0c9..2b8e5d6d 100644 --- a/media/archweb.js +++ b/media/archweb.js @@ -267,22 +267,27 @@ function filter_signoffs() { /* start with all rows, and then remove ones we shouldn't show */ var rows = $('#tbody_signoffs').children(); var all_rows = rows; - $('#signoffs_filter .arch_filter').each(function() { + /* apply arch and repo filters */ + $('#signoffs_filter .arch_filter').add( + '#signoffs_filter .repo_filter').each(function() { if (!$(this).is(':checked')) { rows = rows.not('.' + $(this).val()); } }); + /* and then the slightly more expensive pending check */ if ($('#id_pending').is(':checked')) { rows = rows.has('td.signoff-no'); } /* hide all rows, then show the set we care about */ all_rows.hide(); rows.show(); + $('#filter-count').text(rows.length); /* make sure we update the odd/even styling from sorting */ $('.results').trigger('applyWidgets'); } function filter_signoffs_reset() { $('#signoffs_filter .arch_filter').attr('checked', 'checked'); + $('#signoffs_filter .repo_filter').attr('checked', 'checked'); $('#id_pending').removeAttr('checked'); filter_signoffs(); } diff --git a/packages/views.py b/packages/views.py index aa15d0cf..3c0c2bee 100644 --- a/packages/views.py +++ b/packages/views.py @@ -381,6 +381,7 @@ def signoffs(request): context = { 'signoff_groups': signoff_groups, 'arches': Arch.objects.all(), + 'repo_names': sorted(set(g.target_repo for g in signoff_groups)), } return direct_to_template(request, 'packages/signoffs.html', context) diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index d517e5e3..f4511f75 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -9,7 +9,7 @@

    Package Signoffs

    -

    {{ signoff_groups|length }} signoff group{{ signoff_groups|pluralize }} found. +

    {{ signoff_groups|length }} total signoff group{{ signoff_groups|pluralize }} found. A "signoff group" consists of packages grouped by pkgbase, architecture, and repository.

    @@ -21,9 +21,15 @@

    Filter Displayed Signoffs

    {% endfor %} + {% for repo_name in repo_names %} +
    +
    + {% endfor %}
    -
    +
    +
    +
    {{ signoff_groups|length }} signoff groups displayed.
    @@ -44,7 +50,7 @@

    Filter Displayed Signoffs

    {% for group in signoff_groups %} - + {% pkg_details_link group.package %} {{ group.version }} {{ group.arch.name }} {{ group.target_repo }} -- cgit v1.2.3-54-g00ecf From e565fde00f56c7a01ff55a204a0a56d3ce4bf8b4 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 4 Nov 2011 11:31:35 -0500 Subject: Signoff email: prune empty content Don't send the email at all if there are no packages even in the repository, and don't print empty sections. Signed-off-by: Dan McGee --- packages/management/commands/signoff_report.py | 4 ++++ templates/packages/signoff_report.txt | 13 +++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) (limited to 'packages') diff --git a/packages/management/commands/signoff_report.py b/packages/management/commands/signoff_report.py index 3431dada..3357bc1e 100644 --- a/packages/management/commands/signoff_report.py +++ b/packages/management/commands/signoff_report.py @@ -69,6 +69,10 @@ def generate_report(email, repo_name): new_cutoff = now - timedelta(hours=new_hours) old_cutoff = now - timedelta(days=old_days) + if len(signoff_groups) == 0: + # no need to send an email at all + return + for group in signoff_groups: spec = group.specification if spec.known_bad: diff --git a/templates/packages/signoff_report.txt b/templates/packages/signoff_report.txt index 81020c8f..046c2f1e 100644 --- a/templates/packages/signoff_report.txt +++ b/templates/packages/signoff_report.txt @@ -14,27 +14,28 @@ pkgbase, architecture, and repository; e.g., one PKGBUILD produces one package per architecture, even if it is a split package.) -== New packages in [{{ repo|lower}}] in last {{ new_hours }} hours ({{ new|length }} total) == +{% if new %}== New packages in [{{ repo|lower}}] in last {{ new_hours }} hours ({{ new|length }} total) == {% for group in new %} * {{ group.pkgbase }}-{{ group.version }} ({{ group.arch }}){% endfor %} -{% regroup incomplete by target_repo as by_repo %}{% for target_repo in by_repo %} +{% endif %}{% regroup incomplete by target_repo as by_repo %}{% for target_repo in by_repo %} == Incomplete signoffs for [{{ target_repo.grouper|lower }}] ({{ target_repo.list|length }} total) == {% for group in target_repo.list %} * {{ group.pkgbase }}-{{ group.version }} ({{ group.arch }}) {{ group.completed }}/{{ group.required }} signoffs{% endfor %} {% endfor %} -== Completed signoffs ({{ complete|length }} total) == +{% if complete %}== Completed signoffs ({{ complete|length }} total) == {% for group in complete %} * {{ group.pkgbase }}-{{ group.version }} ({{ group.arch }}){% endfor %} -== All packages in [{{ repo|lower }}] for more than {{ old_days }} days ({{ old|length }} total) == +{% endif %}{% if old %}== All packages in [{{ repo|lower }}] for more than {{ old_days }} days ({{ old|length }} total) == {% for group in old %} * {{ group.pkgbase }}-{{ group.version }} ({{ group.arch }}), since {{ group.last_update|date }}{% endfor %} -{% endautoescape %} -== Top five in signoffs in last {{ new_hours }} hours == + +{% endif %}== Top five in signoffs in last {{ new_hours }} hours == {% for leader in leaders %} {{ forloop.counter }}. {{ leader.user }} - {{ leader.count }} signoffs{% endfor %} +{% endautoescape %} -- cgit v1.2.3-54-g00ecf From 28f72db7be7bf2f54d734c78422e6179f0ce29f1 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 4 Nov 2011 12:38:57 -0500 Subject: Rewrite get_target_repo_map() using raw SQL This improves the shitty query plan brought upon us by MySQL by rewriting it to use JOINs only and no dependent subqueries. Signed-off-by: Dan McGee --- packages/utils.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) (limited to 'packages') diff --git a/packages/utils.py b/packages/utils.py index ddd822e4..b21ac557 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -310,12 +310,25 @@ def get_current_specifications(repos): to_fetch = [row[0] for row in results] return SignoffSpecification.objects.in_bulk(to_fetch).values() -def get_target_repo_map(pkgbases): - package_repos = Package.objects.order_by().values_list( - 'pkgbase', 'repo__name').filter( - repo__testing=False, repo__staging=False, - pkgbase__in=pkgbases).distinct() - return dict(package_repos) +def get_target_repo_map(repos): + sql = """ +SELECT DISTINCT p1.pkgbase, r.name + FROM packages p1 + JOIN repos r ON p1.repo_id = r.id + JOIN packages p2 ON p1.pkgbase = p2.pkgbase + WHERE r.staging = %s + AND r.testing = %s + AND p2.repo_id IN ( + """ + sql += ','.join(['%s' for r in repos]) + sql += ")" + + params = [False, False] + params.extend(r.pk for r in repos) + + cursor = connection.cursor() + cursor.execute(sql, params) + return dict(cursor.fetchall()) def get_signoff_groups(repos=None): if repos is None: @@ -328,8 +341,7 @@ def get_signoff_groups(repos=None): packages = attach_maintainers(packages) # Collect all pkgbase values in testing repos - q_pkgbase = test_pkgs.values('pkgbase') - pkgtorepo = get_target_repo_map(q_pkgbase) + pkgtorepo = get_target_repo_map(repos) # Collect all possible signoffs and specifications for these packages signoffs = get_current_signoffs(repos) -- cgit v1.2.3-54-g00ecf From d80f4236d01f70380f71a46dd98f1f789d91d31c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 10 Nov 2011 18:09:19 -0600 Subject: Add package signoffs JSON view This allows access to the same data (and even a bit more) from the signoffs overview page in a machine-friendly way. Signed-off-by: Dan McGee --- mirrors/utils.py | 3 ++- packages/urls.py | 1 + packages/views.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 2 deletions(-) (limited to 'packages') diff --git a/mirrors/utils.py b/mirrors/utils.py index 686ec581..8518b3ba 100644 --- a/mirrors/utils.py +++ b/mirrors/utils.py @@ -40,7 +40,8 @@ def get_mirror_statuses(cutoff=default_cutoff): last_sync=Max('logs__last_sync'), last_check=Max('logs__check_time'), duration_avg=Avg('logs__duration'), - duration_stddev=StdDev('logs__duration') + #duration_stddev=StdDev('logs__duration') + duration_stddev=Max('logs__duration') ).order_by('-last_sync', '-duration_avg') # The Django ORM makes it really hard to get actual average delay in the diff --git a/packages/urls.py b/packages/urls.py index 4d391a3c..1f25e3fd 100644 --- a/packages/urls.py +++ b/packages/urls.py @@ -18,6 +18,7 @@ urlpatterns = patterns('packages.views', (r'^flaghelp/$', 'flaghelp'), (r'^signoffs/$', 'signoffs', {}, 'package-signoffs'), + (r'^signoffs/json/$', 'signoffs_json', {}, 'package-signoffs-json'), (r'^update/$', 'update'), (r'^$', 'search', {}, 'packages-search'), diff --git a/packages/views.py b/packages/views.py index 3c0c2bee..cac5d076 100644 --- a/packages/views.py +++ b/packages/views.py @@ -29,7 +29,8 @@ from mirrors.models import MirrorUrl from .models import PackageRelation, PackageGroup, SignoffSpecification, Signoff from .utils import (get_group_info, get_differences_info, - get_wrong_permissions, get_signoff_groups, approved_by_signoffs) + get_wrong_permissions, get_signoff_groups, approved_by_signoffs, + PackageSignoffGroup) class PackageJSONEncoder(DjangoJSONEncoder): pkg_attributes = [ 'pkgname', 'pkgbase', 'repo', 'arch', 'pkgver', @@ -371,6 +372,7 @@ def unflag_all(request, name, repo, arch): pkgs.update(flag_date=None) return redirect(pkg) + @permission_required('main.change_package') @never_cache def signoffs(request): @@ -496,6 +498,49 @@ def signoff_options(request, name, repo, arch): } return direct_to_template(request, 'packages/signoff_options.html', context) +class SignoffJSONEncoder(DjangoJSONEncoder): + '''Base JSONEncoder extended to handle all serialization of all classes + related to signoffs.''' + signoff_group_attrs = ['arch', 'last_update', 'maintainers', 'packager', + 'pkgbase', 'repo', 'signoffs', 'target_repo', 'version'] + signoff_spec_attrs = ['required', 'enabled', 'known_bad', 'comments'] + signoff_attrs = ['user', 'created', 'revoked'] + + def default(self, obj): + if isinstance(obj, PackageSignoffGroup): + data = dict((attr, getattr(obj, attr)) + for attr in self.signoff_group_attrs) + data['package_count'] = len(obj.packages) + data['approved'] = obj.approved() + data.update((attr, getattr(obj.specification, attr)) + for attr in self.signoff_spec_attrs) + return data + elif isinstance(obj, Signoff): + data = dict((attr, getattr(obj, attr)) + for attr in self.signoff_attrs) + return data + elif isinstance(obj, Arch) or isinstance(obj, Repo): + return unicode(obj) + elif isinstance(obj, User): + return obj.username + elif isinstance(obj, set): + return list(obj) + return super(SignoffJSONEncoder, self).default(obj) + +@permission_required('main.change_package') +@never_cache +def signoffs_json(request): + signoff_groups = sorted(get_signoff_groups(), key=attrgetter('pkgbase')) + data = { + 'version': 1, + 'signoff_groups': signoff_groups, + } + to_json = simplejson.dumps(data, ensure_ascii=False, + cls=SignoffJSONEncoder) + response = HttpResponse(to_json, mimetype='application/json') + return response + + def flaghelp(request): return direct_to_template(request, 'packages/flaghelp.html') -- cgit v1.2.3-54-g00ecf From 83feb682c2909cbd8c332a9b16aacbc8d696a13a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 10 Nov 2011 18:12:47 -0600 Subject: Move package views into subdirectory This simply moves views.py to views/__init__.py and adjusts the imports accordingly; future patches will split this into multiple files as this module is getting quite large. Signed-off-by: Dan McGee --- packages/views.py | 696 -------------------------------------------- packages/views/__init__.py | 697 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 697 insertions(+), 696 deletions(-) delete mode 100644 packages/views.py create mode 100644 packages/views/__init__.py (limited to 'packages') diff --git a/packages/views.py b/packages/views.py deleted file mode 100644 index cac5d076..00000000 --- a/packages/views.py +++ /dev/null @@ -1,696 +0,0 @@ -from django import forms -from django.contrib import messages -from django.contrib.admin.widgets import AdminDateWidget -from django.contrib.auth.models import User -from django.contrib.auth.decorators import permission_required -from django.conf import settings -from django.core.mail import send_mail -from django.core.serializers.json import DjangoJSONEncoder -from django.db import transaction -from django.db.models import Q -from django.http import HttpResponse, Http404, HttpResponseForbidden -from django.shortcuts import (get_object_or_404, get_list_or_404, - redirect, render) -from django.template import loader, Context -from django.utils import simplejson -from django.views.decorators.cache import never_cache -from django.views.decorators.http import require_POST -from django.views.decorators.vary import vary_on_headers -from django.views.generic import list_detail -from django.views.generic.simple import direct_to_template - -from datetime import datetime -from operator import attrgetter -from string import Template -from urllib import urlencode - -from main.models import Package, PackageFile, Arch, Repo -from main.utils import make_choice -from mirrors.models import MirrorUrl -from .models import PackageRelation, PackageGroup, SignoffSpecification, Signoff -from .utils import (get_group_info, get_differences_info, - get_wrong_permissions, get_signoff_groups, approved_by_signoffs, - PackageSignoffGroup) - -class PackageJSONEncoder(DjangoJSONEncoder): - pkg_attributes = [ 'pkgname', 'pkgbase', 'repo', 'arch', 'pkgver', - 'pkgrel', 'epoch', 'pkgdesc', 'url', 'filename', 'compressed_size', - 'installed_size', 'build_date', 'last_update', 'flag_date' ] - - def default(self, obj): - if hasattr(obj, '__iter__'): - # mainly for queryset serialization - return list(obj) - if isinstance(obj, Package): - data = dict((attr, getattr(obj, attr)) - for attr in self.pkg_attributes) - data['groups'] = obj.groups.all() - return data - if isinstance(obj, PackageFile): - filename = obj.filename or '' - return obj.directory + filename - if isinstance(obj, (Repo, Arch, PackageGroup)): - return obj.name.lower() - return super(PackageJSONEncoder, self).default(obj) - -def opensearch(request): - if request.is_secure(): - domain = "https://%s" % request.META['HTTP_HOST'] - else: - domain = "http://%s" % request.META['HTTP_HOST'] - - return direct_to_template(request, 'packages/opensearch.xml', - {'domain': domain}, - mimetype='application/opensearchdescription+xml') - -@permission_required('main.change_package') -@require_POST -def update(request): - ids = request.POST.getlist('pkgid') - count = 0 - - if request.POST.has_key('adopt'): - repos = request.user.userprofile.allowed_repos.all() - pkgs = Package.objects.filter(id__in=ids, repo__in=repos) - disallowed_pkgs = Package.objects.filter(id__in=ids).exclude( - repo__in=repos) - - if disallowed_pkgs: - messages.warning(request, - "You do not have permission to adopt: %s." % ( - ' '.join([p.pkgname for p in disallowed_pkgs]) - )) - - for pkg in pkgs: - if request.user not in pkg.maintainers: - prel = PackageRelation(pkgbase=pkg.pkgbase, - user=request.user, - type=PackageRelation.MAINTAINER) - count += 1 - prel.save() - - messages.info(request, "%d base packages adopted." % count) - - elif request.POST.has_key('disown'): - # allow disowning regardless of allowed repos, helps things like - # [community] -> [extra] moves - for pkg in Package.objects.filter(id__in=ids): - if request.user in pkg.maintainers: - rels = PackageRelation.objects.filter(pkgbase=pkg.pkgbase, - user=request.user, - type=PackageRelation.MAINTAINER) - count += rels.count() - rels.delete() - - messages.info(request, "%d base packages disowned." % count) - - else: - messages.error(request, "Are you trying to adopt or disown?") - return redirect('/packages/') - -def details(request, name='', repo='', arch=''): - if all([name, repo, arch]): - try: - pkg = Package.objects.select_related( - 'arch', 'repo', 'packager').get(pkgname=name, - repo__name__iexact=repo, arch__name=arch) - return direct_to_template(request, 'packages/details.html', - {'pkg': pkg, }) - except Package.DoesNotExist: - arch = get_object_or_404(Arch, name=arch) - arches = [ arch ] - arches.extend(Arch.objects.filter(agnostic=True)) - repo = get_object_or_404(Repo, name__iexact=repo) - pkgs = Package.objects.normal().filter(pkgbase=name, - repo__testing=repo.testing, repo__staging=repo.staging, - arch__in=arches).order_by('pkgname') - if len(pkgs) == 0: - raise Http404 - context = { - 'list_title': 'Split Package Details', - 'name': name, - 'arch': arch, - 'packages': pkgs, - } - return direct_to_template(request, 'packages/packages_list.html', - context) - else: - pkg_data = [ - ('arch', arch.lower()), - ('repo', repo.lower()), - ('q', name), - ] - # only include non-blank values in the query we generate - pkg_data = [(x, y) for x, y in pkg_data if y] - return redirect("/packages/?%s" % urlencode(pkg_data)) - -def groups(request, arch=None): - arches = [] - if arch: - get_object_or_404(Arch, name=arch, agnostic=False) - arches.append(arch) - grps = get_group_info(arches) - context = { - 'groups': grps, - 'arch': arch, - } - return direct_to_template(request, 'packages/groups.html', context) - -def group_details(request, arch, name): - arch = get_object_or_404(Arch, name=arch) - arches = [ arch ] - arches.extend(Arch.objects.filter(agnostic=True)) - pkgs = Package.objects.normal().filter( - groups__name=name, arch__in=arches).order_by('pkgname') - if len(pkgs) == 0: - raise Http404 - context = { - 'list_title': 'Group Details', - 'name': name, - 'arch': arch, - 'packages': pkgs, - } - return direct_to_template(request, 'packages/packages_list.html', context) - -def coerce_limit_value(value): - if not value: - return None - if value == 'all': - # negative value indicates show all results - return -1 - value = int(value) - if value < 0: - raise ValueError - return value - -class LimitTypedChoiceField(forms.TypedChoiceField): - def valid_value(self, value): - try: - coerce_limit_value(value) - return True - except (ValueError, TypeError): - return False - -class PackageSearchForm(forms.Form): - repo = forms.MultipleChoiceField(required=False) - arch = forms.MultipleChoiceField(required=False) - name = forms.CharField(required=False) - desc = forms.CharField(required=False) - q = forms.CharField(required=False) - maintainer = forms.ChoiceField(required=False) - packager = forms.ChoiceField(required=False) - last_update = forms.DateField(required=False, widget=AdminDateWidget(), - label='Last Updated After') - flagged = forms.ChoiceField( - choices=[('', 'All')] + make_choice(['Flagged', 'Not Flagged']), - required=False) - signed = forms.ChoiceField( - choices=[('', 'All')] + make_choice(['Signed', 'Unsigned']), - required=False) - limit = LimitTypedChoiceField( - choices=make_choice([50, 100, 250]) + [('all', 'All')], - coerce=coerce_limit_value, - required=False, - initial=50) - - def __init__(self, *args, **kwargs): - super(PackageSearchForm, self).__init__(*args, **kwargs) - self.fields['repo'].choices = make_choice( - [repo.name for repo in Repo.objects.all()]) - self.fields['arch'].choices = make_choice( - [arch.name for arch in Arch.objects.all()]) - self.fields['q'].widget.attrs.update({"size": "30"}) - maints = User.objects.filter(is_active=True).order_by('username') - self.fields['maintainer'].choices = \ - [('', 'All'), ('orphan', 'Orphan')] + \ - [(m.username, m.get_full_name()) for m in maints] - self.fields['packager'].choices = \ - [('', 'All'), ('unknown', 'Unknown')] + \ - [(m.username, m.get_full_name()) for m in maints] - -def search(request, page=None): - limit = 50 - packages = Package.objects.normal() - - if request.GET: - form = PackageSearchForm(data=request.GET) - if form.is_valid(): - if form.cleaned_data['repo']: - packages = packages.filter( - repo__name__in=form.cleaned_data['repo']) - - if form.cleaned_data['arch']: - packages = packages.filter( - arch__name__in=form.cleaned_data['arch']) - - if form.cleaned_data['maintainer'] == 'orphan': - inner_q = PackageRelation.objects.all().values('pkgbase') - packages = packages.exclude(pkgbase__in=inner_q) - elif form.cleaned_data['maintainer']: - inner_q = PackageRelation.objects.filter( - user__username=form.cleaned_data['maintainer']).values('pkgbase') - packages = packages.filter(pkgbase__in=inner_q) - - if form.cleaned_data['packager'] == 'unknown': - packages = packages.filter(packager__isnull=True) - elif form.cleaned_data['packager']: - packages = packages.filter( - packager__username=form.cleaned_data['packager']) - - if form.cleaned_data['flagged'] == 'Flagged': - packages = packages.filter(flag_date__isnull=False) - elif form.cleaned_data['flagged'] == 'Not Flagged': - packages = packages.filter(flag_date__isnull=True) - - if form.cleaned_data['signed'] == 'Signed': - packages = packages.filter(pgp_signature__isnull=False) - elif form.cleaned_data['signed'] == 'Unsigned': - packages = packages.filter(pgp_signature__isnull=True) - - if form.cleaned_data['last_update']: - lu = form.cleaned_data['last_update'] - packages = packages.filter(last_update__gte= - datetime(lu.year, lu.month, lu.day, 0, 0)) - - if form.cleaned_data['name']: - name = form.cleaned_data['name'] - packages = packages.filter(pkgname__icontains=name) - - if form.cleaned_data['desc']: - desc = form.cleaned_data['desc'] - packages = packages.filter(pkgdesc__icontains=desc) - - if form.cleaned_data['q']: - query = form.cleaned_data['q'] - q = Q(pkgname__icontains=query) | Q(pkgdesc__icontains=query) - packages = packages.filter(q) - - asked_limit = form.cleaned_data['limit'] - if asked_limit and asked_limit < 0: - limit = None - elif asked_limit: - limit = asked_limit - else: - # Form had errors, don't return any results, just the busted form - packages = Package.objects.none() - else: - form = PackageSearchForm() - - current_query = request.GET.urlencode() - page_dict = { - 'search_form': form, - 'current_query': current_query - } - allowed_sort = ["arch", "repo", "pkgname", "pkgbase", - "compressed_size", "installed_size", - "build_date", "last_update", "flag_date"] - allowed_sort += ["-" + s for s in allowed_sort] - sort = request.GET.get('sort', None) - if sort in allowed_sort: - packages = packages.order_by(sort) - page_dict['sort'] = sort - else: - packages = packages.order_by('pkgname') - - return list_detail.object_list(request, packages, - template_name="packages/search.html", - page=page, - paginate_by=limit, - template_object_name="package", - extra_context=page_dict) - -@vary_on_headers('X-Requested-With') -def files(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) - fileslist = PackageFile.objects.filter(pkg=pkg).order_by('directory', 'filename') - context = { - 'pkg': pkg, - 'files': fileslist, - } - template = 'packages/files.html' - if request.is_ajax(): - template = 'packages/files-list.html' - return direct_to_template(request, template, context) - -def details_json(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) - to_json = simplejson.dumps(pkg, ensure_ascii=False, - cls=PackageJSONEncoder) - return HttpResponse(to_json, mimetype='application/json') - -def files_json(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) - fileslist = PackageFile.objects.filter(pkg=pkg).order_by('directory', 'filename') - data = { - 'pkgname': pkg.pkgname, - 'repo': pkg.repo.name.lower(), - 'arch': pkg.arch.name.lower(), - 'files': fileslist, - } - to_json = simplejson.dumps(data, ensure_ascii=False, - cls=PackageJSONEncoder) - return HttpResponse(to_json, mimetype='application/json') - -@permission_required('main.change_package') -def unflag(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) - pkg.flag_date = None - pkg.save() - return redirect(pkg) - -@permission_required('main.change_package') -def unflag_all(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) - # find all packages from (hopefully) the same PKGBUILD - pkgs = Package.objects.filter(pkgbase=pkg.pkgbase, - repo__testing=pkg.repo.testing, repo__staging=pkg.repo.staging) - pkgs.update(flag_date=None) - return redirect(pkg) - - -@permission_required('main.change_package') -@never_cache -def signoffs(request): - signoff_groups = sorted(get_signoff_groups(), key=attrgetter('pkgbase')) - for group in signoff_groups: - group.user = request.user - - context = { - 'signoff_groups': signoff_groups, - 'arches': Arch.objects.all(), - 'repo_names': sorted(set(g.target_repo for g in signoff_groups)), - } - return direct_to_template(request, 'packages/signoffs.html', context) - -@permission_required('main.change_package') -@never_cache -def signoff_package(request, name, repo, arch, revoke=False): - packages = get_list_or_404(Package, pkgbase=name, - arch__name=arch, repo__name__iexact=repo, repo__testing=True) - package = packages[0] - - spec = SignoffSpecification.objects.get_or_default_from_package(package) - - if revoke: - try: - signoff = Signoff.objects.get_from_package( - package, request.user, False) - except Signoff.DoesNotExist: - raise Http404 - signoff.revoked = datetime.utcnow() - signoff.save() - created = False - else: - # ensure we should even be accepting signoffs - if spec.known_bad or not spec.enabled: - return render(request, '403.html', status=403) - signoff, created = Signoff.objects.get_or_create_from_package( - package, request.user) - - all_signoffs = Signoff.objects.for_package(package) - - if request.is_ajax(): - data = { - 'created': created, - 'revoked': bool(signoff.revoked), - 'approved': approved_by_signoffs(all_signoffs, spec), - 'required': spec.required, - 'enabled': spec.enabled, - 'known_bad': spec.known_bad, - 'user': str(request.user), - } - return HttpResponse(simplejson.dumps(data, ensure_ascii=False), - mimetype='application/json') - - return redirect('package-signoffs') - -class SignoffOptionsForm(forms.ModelForm): - apply_all = forms.BooleanField(required=False, - help_text="Apply these options to all architectures?") - - class Meta: - model = SignoffSpecification - fields = ('required', 'enabled', 'known_bad', 'comments') - -def _signoff_options_all(request, name, repo): - seen_ids = set() - with transaction.commit_on_success(): - # find or create a specification for all architectures, then - # graft the form data onto them - packages = Package.objects.filter(pkgbase=name, - repo__name__iexact=repo, repo__testing=True) - for package in packages: - try: - spec = SignoffSpecification.objects.get_from_package(package) - if spec.pk in seen_ids: - continue - except SignoffSpecification.DoesNotExist: - spec = SignoffSpecification(pkgbase=package.pkgbase, - pkgver=package.pkgver, pkgrel=package.pkgrel, - epoch=package.epoch, arch=package.arch, - repo=package.repo) - spec.user = request.user - form = SignoffOptionsForm(request.POST, instance=spec) - if form.is_valid(): - form.save() - seen_ids.add(form.instance.pk) - -@permission_required('main.change_package') -@never_cache -def signoff_options(request, name, repo, arch): - packages = get_list_or_404(Package, pkgbase=name, - arch__name=arch, repo__name__iexact=repo, repo__testing=True) - package = packages[0] - - if request.user != package.packager and \ - request.user not in package.maintainers: - return render(request, '403.html', status=403) - - try: - spec = SignoffSpecification.objects.get_from_package(package) - except SignoffSpecification.DoesNotExist: - # create a fake one, but don't save it just yet - spec = SignoffSpecification(pkgbase=package.pkgbase, - pkgver=package.pkgver, pkgrel=package.pkgrel, - epoch=package.epoch, arch=package.arch, repo=package.repo) - spec.user = request.user - - if request.POST: - form = SignoffOptionsForm(request.POST, instance=spec) - if form.is_valid(): - if form.cleaned_data['apply_all']: - _signoff_options_all(request, name, repo) - else: - form.save() - return redirect('package-signoffs') - else: - form = SignoffOptionsForm(instance=spec) - - context = { - 'packages': packages, - 'package': package, - 'form': form, - } - return direct_to_template(request, 'packages/signoff_options.html', context) - -class SignoffJSONEncoder(DjangoJSONEncoder): - '''Base JSONEncoder extended to handle all serialization of all classes - related to signoffs.''' - signoff_group_attrs = ['arch', 'last_update', 'maintainers', 'packager', - 'pkgbase', 'repo', 'signoffs', 'target_repo', 'version'] - signoff_spec_attrs = ['required', 'enabled', 'known_bad', 'comments'] - signoff_attrs = ['user', 'created', 'revoked'] - - def default(self, obj): - if isinstance(obj, PackageSignoffGroup): - data = dict((attr, getattr(obj, attr)) - for attr in self.signoff_group_attrs) - data['package_count'] = len(obj.packages) - data['approved'] = obj.approved() - data.update((attr, getattr(obj.specification, attr)) - for attr in self.signoff_spec_attrs) - return data - elif isinstance(obj, Signoff): - data = dict((attr, getattr(obj, attr)) - for attr in self.signoff_attrs) - return data - elif isinstance(obj, Arch) or isinstance(obj, Repo): - return unicode(obj) - elif isinstance(obj, User): - return obj.username - elif isinstance(obj, set): - return list(obj) - return super(SignoffJSONEncoder, self).default(obj) - -@permission_required('main.change_package') -@never_cache -def signoffs_json(request): - signoff_groups = sorted(get_signoff_groups(), key=attrgetter('pkgbase')) - data = { - 'version': 1, - 'signoff_groups': signoff_groups, - } - to_json = simplejson.dumps(data, ensure_ascii=False, - cls=SignoffJSONEncoder) - response = HttpResponse(to_json, mimetype='application/json') - return response - - -def flaghelp(request): - return direct_to_template(request, 'packages/flaghelp.html') - -class FlagForm(forms.Form): - email = forms.EmailField(label='* E-mail Address') - usermessage = forms.CharField(label='Message To Dev', - widget=forms.Textarea, required=False) - # The field below is used to filter out bots that blindly fill out all input elements - website = forms.CharField(label='', - widget=forms.TextInput(attrs={'style': 'display:none;'}), - required=False) - -@never_cache -def flag(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) - if pkg.flag_date is not None: - # already flagged. do nothing. - return direct_to_template(request, 'packages/flagged.html', {'pkg': pkg}) - # find all packages from (hopefully) the same PKGBUILD - pkgs = Package.objects.normal().filter( - pkgbase=pkg.pkgbase, flag_date__isnull=True, - repo__testing=pkg.repo.testing, - repo__staging=pkg.repo.staging).order_by( - 'pkgname', 'repo__name', 'arch__name') - - if request.POST: - form = FlagForm(request.POST) - if form.is_valid() and form.cleaned_data['website'] == '': - # save the package list for later use - flagged_pkgs = list(pkgs) - pkgs.update(flag_date=datetime.utcnow()) - - maints = pkg.maintainers - if not maints: - toemail = settings.NOTIFICATIONS - subject = 'Orphan %s package [%s] marked out-of-date' % \ - (pkg.repo.name, pkg.pkgname) - else: - toemail = [] - subject = '%s package [%s] marked out-of-date' % \ - (pkg.repo.name, pkg.pkgname) - for maint in maints: - if maint.get_profile().notify == True: - toemail.append(maint.email) - - if toemail: - # send notification email to the maintainers - t = loader.get_template('packages/outofdate.txt') - c = Context({ - 'email': form.cleaned_data['email'], - 'message': form.cleaned_data['usermessage'], - 'pkg': pkg, - 'packages': flagged_pkgs, - }) - send_mail(subject, - t.render(c), - 'Arch Website Notification ', - toemail, - fail_silently=True) - - return redirect('package-flag-confirmed', name=name, repo=repo, - arch=arch) - else: - form = FlagForm() - - context = { - 'package': pkg, - 'packages': pkgs, - 'form': form - } - return direct_to_template(request, 'packages/flag.html', context) - -def flag_confirmed(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) - pkgs = Package.objects.normal().filter( - pkgbase=pkg.pkgbase, flag_date=pkg.flag_date, - repo__testing=pkg.repo.testing, - repo__staging=pkg.repo.staging).order_by( - 'pkgname', 'repo__name', 'arch__name') - - context = {'package': pkg, 'packages': pkgs} - - return direct_to_template(request, 'packages/flag_confirmed.html', context) - -def download(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) - mirror_urls = MirrorUrl.objects.filter( - mirror__public=True, mirror__active=True, - protocol__protocol__iexact='HTTP') - # look first for an 'Any' URL, then fall back to any HTTP URL - filtered_urls = mirror_urls.filter(mirror__country='Any')[:1] - if not filtered_urls: - filtered_urls = mirror_urls[:1] - if not filtered_urls: - raise Http404 - arch = pkg.arch.name - if pkg.arch.agnostic: - # grab the first non-any arch to fake the download path - arch = Arch.objects.exclude(agnostic=True)[0].name - values = { - 'host': filtered_urls[0].url, - 'arch': arch, - 'repo': pkg.repo.name.lower(), - 'file': pkg.filename, - } - url = Template('${host}${repo}/os/${arch}/${file}').substitute(values) - return redirect(url) - -def arch_differences(request): - # TODO: we have some hardcoded magic here with respect to the arches. - arch_a = Arch.objects.get(name='i686') - arch_b = Arch.objects.get(name='x86_64') - differences = get_differences_info(arch_a, arch_b) - context = { - 'arch_a': arch_a, - 'arch_b': arch_b, - 'differences': differences, - } - return direct_to_template(request, 'packages/differences.html', context) - -@permission_required('main.change_package') -@never_cache -def stale_relations(request): - relations = PackageRelation.objects.select_related('user') - pkgbases = Package.objects.all().values('pkgbase') - - inactive_user = relations.filter(user__is_active=False) - missing_pkgbase = relations.exclude( - pkgbase__in=pkgbases).order_by('pkgbase') - wrong_permissions = get_wrong_permissions() - - context = { - 'inactive_user': inactive_user, - 'missing_pkgbase': missing_pkgbase, - 'wrong_permissions': wrong_permissions, - } - return direct_to_template(request, 'packages/stale_relations.html', context) - -@permission_required('packages.delete_packagerelation') -@require_POST -def stale_relations_update(request): - ids = set(request.POST.getlist('relation_id')) - - if ids: - PackageRelation.objects.filter(id__in=ids).delete() - - messages.info(request, "%d package relations deleted." % len(ids)) - return redirect('/packages/stale_relations/') - -# vim: set ts=4 sw=4 et: diff --git a/packages/views/__init__.py b/packages/views/__init__.py new file mode 100644 index 00000000..7b8e4847 --- /dev/null +++ b/packages/views/__init__.py @@ -0,0 +1,697 @@ +from django import forms +from django.contrib import messages +from django.contrib.admin.widgets import AdminDateWidget +from django.contrib.auth.models import User +from django.contrib.auth.decorators import permission_required +from django.conf import settings +from django.core.mail import send_mail +from django.core.serializers.json import DjangoJSONEncoder +from django.db import transaction +from django.db.models import Q +from django.http import HttpResponse, Http404, HttpResponseForbidden +from django.shortcuts import (get_object_or_404, get_list_or_404, + redirect, render) +from django.template import loader, Context +from django.utils import simplejson +from django.views.decorators.cache import never_cache +from django.views.decorators.http import require_POST +from django.views.decorators.vary import vary_on_headers +from django.views.generic import list_detail +from django.views.generic.simple import direct_to_template + +from datetime import datetime +from operator import attrgetter +from string import Template +from urllib import urlencode + +from main.models import Package, PackageFile, Arch, Repo +from main.utils import make_choice +from mirrors.models import MirrorUrl +from ..models import (PackageRelation, PackageGroup, + SignoffSpecification, Signoff) +from ..utils import (get_group_info, get_differences_info, + get_wrong_permissions, get_signoff_groups, approved_by_signoffs, + PackageSignoffGroup) + +class PackageJSONEncoder(DjangoJSONEncoder): + pkg_attributes = [ 'pkgname', 'pkgbase', 'repo', 'arch', 'pkgver', + 'pkgrel', 'epoch', 'pkgdesc', 'url', 'filename', 'compressed_size', + 'installed_size', 'build_date', 'last_update', 'flag_date' ] + + def default(self, obj): + if hasattr(obj, '__iter__'): + # mainly for queryset serialization + return list(obj) + if isinstance(obj, Package): + data = dict((attr, getattr(obj, attr)) + for attr in self.pkg_attributes) + data['groups'] = obj.groups.all() + return data + if isinstance(obj, PackageFile): + filename = obj.filename or '' + return obj.directory + filename + if isinstance(obj, (Repo, Arch, PackageGroup)): + return obj.name.lower() + return super(PackageJSONEncoder, self).default(obj) + +def opensearch(request): + if request.is_secure(): + domain = "https://%s" % request.META['HTTP_HOST'] + else: + domain = "http://%s" % request.META['HTTP_HOST'] + + return direct_to_template(request, 'packages/opensearch.xml', + {'domain': domain}, + mimetype='application/opensearchdescription+xml') + +@permission_required('main.change_package') +@require_POST +def update(request): + ids = request.POST.getlist('pkgid') + count = 0 + + if request.POST.has_key('adopt'): + repos = request.user.userprofile.allowed_repos.all() + pkgs = Package.objects.filter(id__in=ids, repo__in=repos) + disallowed_pkgs = Package.objects.filter(id__in=ids).exclude( + repo__in=repos) + + if disallowed_pkgs: + messages.warning(request, + "You do not have permission to adopt: %s." % ( + ' '.join([p.pkgname for p in disallowed_pkgs]) + )) + + for pkg in pkgs: + if request.user not in pkg.maintainers: + prel = PackageRelation(pkgbase=pkg.pkgbase, + user=request.user, + type=PackageRelation.MAINTAINER) + count += 1 + prel.save() + + messages.info(request, "%d base packages adopted." % count) + + elif request.POST.has_key('disown'): + # allow disowning regardless of allowed repos, helps things like + # [community] -> [extra] moves + for pkg in Package.objects.filter(id__in=ids): + if request.user in pkg.maintainers: + rels = PackageRelation.objects.filter(pkgbase=pkg.pkgbase, + user=request.user, + type=PackageRelation.MAINTAINER) + count += rels.count() + rels.delete() + + messages.info(request, "%d base packages disowned." % count) + + else: + messages.error(request, "Are you trying to adopt or disown?") + return redirect('/packages/') + +def details(request, name='', repo='', arch=''): + if all([name, repo, arch]): + try: + pkg = Package.objects.select_related( + 'arch', 'repo', 'packager').get(pkgname=name, + repo__name__iexact=repo, arch__name=arch) + return direct_to_template(request, 'packages/details.html', + {'pkg': pkg, }) + except Package.DoesNotExist: + arch = get_object_or_404(Arch, name=arch) + arches = [ arch ] + arches.extend(Arch.objects.filter(agnostic=True)) + repo = get_object_or_404(Repo, name__iexact=repo) + pkgs = Package.objects.normal().filter(pkgbase=name, + repo__testing=repo.testing, repo__staging=repo.staging, + arch__in=arches).order_by('pkgname') + if len(pkgs) == 0: + raise Http404 + context = { + 'list_title': 'Split Package Details', + 'name': name, + 'arch': arch, + 'packages': pkgs, + } + return direct_to_template(request, 'packages/packages_list.html', + context) + else: + pkg_data = [ + ('arch', arch.lower()), + ('repo', repo.lower()), + ('q', name), + ] + # only include non-blank values in the query we generate + pkg_data = [(x, y) for x, y in pkg_data if y] + return redirect("/packages/?%s" % urlencode(pkg_data)) + +def groups(request, arch=None): + arches = [] + if arch: + get_object_or_404(Arch, name=arch, agnostic=False) + arches.append(arch) + grps = get_group_info(arches) + context = { + 'groups': grps, + 'arch': arch, + } + return direct_to_template(request, 'packages/groups.html', context) + +def group_details(request, arch, name): + arch = get_object_or_404(Arch, name=arch) + arches = [ arch ] + arches.extend(Arch.objects.filter(agnostic=True)) + pkgs = Package.objects.normal().filter( + groups__name=name, arch__in=arches).order_by('pkgname') + if len(pkgs) == 0: + raise Http404 + context = { + 'list_title': 'Group Details', + 'name': name, + 'arch': arch, + 'packages': pkgs, + } + return direct_to_template(request, 'packages/packages_list.html', context) + +def coerce_limit_value(value): + if not value: + return None + if value == 'all': + # negative value indicates show all results + return -1 + value = int(value) + if value < 0: + raise ValueError + return value + +class LimitTypedChoiceField(forms.TypedChoiceField): + def valid_value(self, value): + try: + coerce_limit_value(value) + return True + except (ValueError, TypeError): + return False + +class PackageSearchForm(forms.Form): + repo = forms.MultipleChoiceField(required=False) + arch = forms.MultipleChoiceField(required=False) + name = forms.CharField(required=False) + desc = forms.CharField(required=False) + q = forms.CharField(required=False) + maintainer = forms.ChoiceField(required=False) + packager = forms.ChoiceField(required=False) + last_update = forms.DateField(required=False, widget=AdminDateWidget(), + label='Last Updated After') + flagged = forms.ChoiceField( + choices=[('', 'All')] + make_choice(['Flagged', 'Not Flagged']), + required=False) + signed = forms.ChoiceField( + choices=[('', 'All')] + make_choice(['Signed', 'Unsigned']), + required=False) + limit = LimitTypedChoiceField( + choices=make_choice([50, 100, 250]) + [('all', 'All')], + coerce=coerce_limit_value, + required=False, + initial=50) + + def __init__(self, *args, **kwargs): + super(PackageSearchForm, self).__init__(*args, **kwargs) + self.fields['repo'].choices = make_choice( + [repo.name for repo in Repo.objects.all()]) + self.fields['arch'].choices = make_choice( + [arch.name for arch in Arch.objects.all()]) + self.fields['q'].widget.attrs.update({"size": "30"}) + maints = User.objects.filter(is_active=True).order_by('username') + self.fields['maintainer'].choices = \ + [('', 'All'), ('orphan', 'Orphan')] + \ + [(m.username, m.get_full_name()) for m in maints] + self.fields['packager'].choices = \ + [('', 'All'), ('unknown', 'Unknown')] + \ + [(m.username, m.get_full_name()) for m in maints] + +def search(request, page=None): + limit = 50 + packages = Package.objects.normal() + + if request.GET: + form = PackageSearchForm(data=request.GET) + if form.is_valid(): + if form.cleaned_data['repo']: + packages = packages.filter( + repo__name__in=form.cleaned_data['repo']) + + if form.cleaned_data['arch']: + packages = packages.filter( + arch__name__in=form.cleaned_data['arch']) + + if form.cleaned_data['maintainer'] == 'orphan': + inner_q = PackageRelation.objects.all().values('pkgbase') + packages = packages.exclude(pkgbase__in=inner_q) + elif form.cleaned_data['maintainer']: + inner_q = PackageRelation.objects.filter( + user__username=form.cleaned_data['maintainer']).values('pkgbase') + packages = packages.filter(pkgbase__in=inner_q) + + if form.cleaned_data['packager'] == 'unknown': + packages = packages.filter(packager__isnull=True) + elif form.cleaned_data['packager']: + packages = packages.filter( + packager__username=form.cleaned_data['packager']) + + if form.cleaned_data['flagged'] == 'Flagged': + packages = packages.filter(flag_date__isnull=False) + elif form.cleaned_data['flagged'] == 'Not Flagged': + packages = packages.filter(flag_date__isnull=True) + + if form.cleaned_data['signed'] == 'Signed': + packages = packages.filter(pgp_signature__isnull=False) + elif form.cleaned_data['signed'] == 'Unsigned': + packages = packages.filter(pgp_signature__isnull=True) + + if form.cleaned_data['last_update']: + lu = form.cleaned_data['last_update'] + packages = packages.filter(last_update__gte= + datetime(lu.year, lu.month, lu.day, 0, 0)) + + if form.cleaned_data['name']: + name = form.cleaned_data['name'] + packages = packages.filter(pkgname__icontains=name) + + if form.cleaned_data['desc']: + desc = form.cleaned_data['desc'] + packages = packages.filter(pkgdesc__icontains=desc) + + if form.cleaned_data['q']: + query = form.cleaned_data['q'] + q = Q(pkgname__icontains=query) | Q(pkgdesc__icontains=query) + packages = packages.filter(q) + + asked_limit = form.cleaned_data['limit'] + if asked_limit and asked_limit < 0: + limit = None + elif asked_limit: + limit = asked_limit + else: + # Form had errors, don't return any results, just the busted form + packages = Package.objects.none() + else: + form = PackageSearchForm() + + current_query = request.GET.urlencode() + page_dict = { + 'search_form': form, + 'current_query': current_query + } + allowed_sort = ["arch", "repo", "pkgname", "pkgbase", + "compressed_size", "installed_size", + "build_date", "last_update", "flag_date"] + allowed_sort += ["-" + s for s in allowed_sort] + sort = request.GET.get('sort', None) + if sort in allowed_sort: + packages = packages.order_by(sort) + page_dict['sort'] = sort + else: + packages = packages.order_by('pkgname') + + return list_detail.object_list(request, packages, + template_name="packages/search.html", + page=page, + paginate_by=limit, + template_object_name="package", + extra_context=page_dict) + +@vary_on_headers('X-Requested-With') +def files(request, name, repo, arch): + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) + fileslist = PackageFile.objects.filter(pkg=pkg).order_by('directory', 'filename') + context = { + 'pkg': pkg, + 'files': fileslist, + } + template = 'packages/files.html' + if request.is_ajax(): + template = 'packages/files-list.html' + return direct_to_template(request, template, context) + +def details_json(request, name, repo, arch): + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) + to_json = simplejson.dumps(pkg, ensure_ascii=False, + cls=PackageJSONEncoder) + return HttpResponse(to_json, mimetype='application/json') + +def files_json(request, name, repo, arch): + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) + fileslist = PackageFile.objects.filter(pkg=pkg).order_by('directory', 'filename') + data = { + 'pkgname': pkg.pkgname, + 'repo': pkg.repo.name.lower(), + 'arch': pkg.arch.name.lower(), + 'files': fileslist, + } + to_json = simplejson.dumps(data, ensure_ascii=False, + cls=PackageJSONEncoder) + return HttpResponse(to_json, mimetype='application/json') + +@permission_required('main.change_package') +def unflag(request, name, repo, arch): + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) + pkg.flag_date = None + pkg.save() + return redirect(pkg) + +@permission_required('main.change_package') +def unflag_all(request, name, repo, arch): + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) + # find all packages from (hopefully) the same PKGBUILD + pkgs = Package.objects.filter(pkgbase=pkg.pkgbase, + repo__testing=pkg.repo.testing, repo__staging=pkg.repo.staging) + pkgs.update(flag_date=None) + return redirect(pkg) + + +@permission_required('main.change_package') +@never_cache +def signoffs(request): + signoff_groups = sorted(get_signoff_groups(), key=attrgetter('pkgbase')) + for group in signoff_groups: + group.user = request.user + + context = { + 'signoff_groups': signoff_groups, + 'arches': Arch.objects.all(), + 'repo_names': sorted(set(g.target_repo for g in signoff_groups)), + } + return direct_to_template(request, 'packages/signoffs.html', context) + +@permission_required('main.change_package') +@never_cache +def signoff_package(request, name, repo, arch, revoke=False): + packages = get_list_or_404(Package, pkgbase=name, + arch__name=arch, repo__name__iexact=repo, repo__testing=True) + package = packages[0] + + spec = SignoffSpecification.objects.get_or_default_from_package(package) + + if revoke: + try: + signoff = Signoff.objects.get_from_package( + package, request.user, False) + except Signoff.DoesNotExist: + raise Http404 + signoff.revoked = datetime.utcnow() + signoff.save() + created = False + else: + # ensure we should even be accepting signoffs + if spec.known_bad or not spec.enabled: + return render(request, '403.html', status=403) + signoff, created = Signoff.objects.get_or_create_from_package( + package, request.user) + + all_signoffs = Signoff.objects.for_package(package) + + if request.is_ajax(): + data = { + 'created': created, + 'revoked': bool(signoff.revoked), + 'approved': approved_by_signoffs(all_signoffs, spec), + 'required': spec.required, + 'enabled': spec.enabled, + 'known_bad': spec.known_bad, + 'user': str(request.user), + } + return HttpResponse(simplejson.dumps(data, ensure_ascii=False), + mimetype='application/json') + + return redirect('package-signoffs') + +class SignoffOptionsForm(forms.ModelForm): + apply_all = forms.BooleanField(required=False, + help_text="Apply these options to all architectures?") + + class Meta: + model = SignoffSpecification + fields = ('required', 'enabled', 'known_bad', 'comments') + +def _signoff_options_all(request, name, repo): + seen_ids = set() + with transaction.commit_on_success(): + # find or create a specification for all architectures, then + # graft the form data onto them + packages = Package.objects.filter(pkgbase=name, + repo__name__iexact=repo, repo__testing=True) + for package in packages: + try: + spec = SignoffSpecification.objects.get_from_package(package) + if spec.pk in seen_ids: + continue + except SignoffSpecification.DoesNotExist: + spec = SignoffSpecification(pkgbase=package.pkgbase, + pkgver=package.pkgver, pkgrel=package.pkgrel, + epoch=package.epoch, arch=package.arch, + repo=package.repo) + spec.user = request.user + form = SignoffOptionsForm(request.POST, instance=spec) + if form.is_valid(): + form.save() + seen_ids.add(form.instance.pk) + +@permission_required('main.change_package') +@never_cache +def signoff_options(request, name, repo, arch): + packages = get_list_or_404(Package, pkgbase=name, + arch__name=arch, repo__name__iexact=repo, repo__testing=True) + package = packages[0] + + if request.user != package.packager and \ + request.user not in package.maintainers: + return render(request, '403.html', status=403) + + try: + spec = SignoffSpecification.objects.get_from_package(package) + except SignoffSpecification.DoesNotExist: + # create a fake one, but don't save it just yet + spec = SignoffSpecification(pkgbase=package.pkgbase, + pkgver=package.pkgver, pkgrel=package.pkgrel, + epoch=package.epoch, arch=package.arch, repo=package.repo) + spec.user = request.user + + if request.POST: + form = SignoffOptionsForm(request.POST, instance=spec) + if form.is_valid(): + if form.cleaned_data['apply_all']: + _signoff_options_all(request, name, repo) + else: + form.save() + return redirect('package-signoffs') + else: + form = SignoffOptionsForm(instance=spec) + + context = { + 'packages': packages, + 'package': package, + 'form': form, + } + return direct_to_template(request, 'packages/signoff_options.html', context) + +class SignoffJSONEncoder(DjangoJSONEncoder): + '''Base JSONEncoder extended to handle all serialization of all classes + related to signoffs.''' + signoff_group_attrs = ['arch', 'last_update', 'maintainers', 'packager', + 'pkgbase', 'repo', 'signoffs', 'target_repo', 'version'] + signoff_spec_attrs = ['required', 'enabled', 'known_bad', 'comments'] + signoff_attrs = ['user', 'created', 'revoked'] + + def default(self, obj): + if isinstance(obj, PackageSignoffGroup): + data = dict((attr, getattr(obj, attr)) + for attr in self.signoff_group_attrs) + data['package_count'] = len(obj.packages) + data['approved'] = obj.approved() + data.update((attr, getattr(obj.specification, attr)) + for attr in self.signoff_spec_attrs) + return data + elif isinstance(obj, Signoff): + data = dict((attr, getattr(obj, attr)) + for attr in self.signoff_attrs) + return data + elif isinstance(obj, Arch) or isinstance(obj, Repo): + return unicode(obj) + elif isinstance(obj, User): + return obj.username + elif isinstance(obj, set): + return list(obj) + return super(SignoffJSONEncoder, self).default(obj) + +@permission_required('main.change_package') +@never_cache +def signoffs_json(request): + signoff_groups = sorted(get_signoff_groups(), key=attrgetter('pkgbase')) + data = { + 'version': 1, + 'signoff_groups': signoff_groups, + } + to_json = simplejson.dumps(data, ensure_ascii=False, + cls=SignoffJSONEncoder) + response = HttpResponse(to_json, mimetype='application/json') + return response + + +def flaghelp(request): + return direct_to_template(request, 'packages/flaghelp.html') + +class FlagForm(forms.Form): + email = forms.EmailField(label='* E-mail Address') + usermessage = forms.CharField(label='Message To Dev', + widget=forms.Textarea, required=False) + # The field below is used to filter out bots that blindly fill out all input elements + website = forms.CharField(label='', + widget=forms.TextInput(attrs={'style': 'display:none;'}), + required=False) + +@never_cache +def flag(request, name, repo, arch): + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) + if pkg.flag_date is not None: + # already flagged. do nothing. + return direct_to_template(request, 'packages/flagged.html', {'pkg': pkg}) + # find all packages from (hopefully) the same PKGBUILD + pkgs = Package.objects.normal().filter( + pkgbase=pkg.pkgbase, flag_date__isnull=True, + repo__testing=pkg.repo.testing, + repo__staging=pkg.repo.staging).order_by( + 'pkgname', 'repo__name', 'arch__name') + + if request.POST: + form = FlagForm(request.POST) + if form.is_valid() and form.cleaned_data['website'] == '': + # save the package list for later use + flagged_pkgs = list(pkgs) + pkgs.update(flag_date=datetime.utcnow()) + + maints = pkg.maintainers + if not maints: + toemail = settings.NOTIFICATIONS + subject = 'Orphan %s package [%s] marked out-of-date' % \ + (pkg.repo.name, pkg.pkgname) + else: + toemail = [] + subject = '%s package [%s] marked out-of-date' % \ + (pkg.repo.name, pkg.pkgname) + for maint in maints: + if maint.get_profile().notify == True: + toemail.append(maint.email) + + if toemail: + # send notification email to the maintainers + t = loader.get_template('packages/outofdate.txt') + c = Context({ + 'email': form.cleaned_data['email'], + 'message': form.cleaned_data['usermessage'], + 'pkg': pkg, + 'packages': flagged_pkgs, + }) + send_mail(subject, + t.render(c), + 'Arch Website Notification ', + toemail, + fail_silently=True) + + return redirect('package-flag-confirmed', name=name, repo=repo, + arch=arch) + else: + form = FlagForm() + + context = { + 'package': pkg, + 'packages': pkgs, + 'form': form + } + return direct_to_template(request, 'packages/flag.html', context) + +def flag_confirmed(request, name, repo, arch): + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) + pkgs = Package.objects.normal().filter( + pkgbase=pkg.pkgbase, flag_date=pkg.flag_date, + repo__testing=pkg.repo.testing, + repo__staging=pkg.repo.staging).order_by( + 'pkgname', 'repo__name', 'arch__name') + + context = {'package': pkg, 'packages': pkgs} + + return direct_to_template(request, 'packages/flag_confirmed.html', context) + +def download(request, name, repo, arch): + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) + mirror_urls = MirrorUrl.objects.filter( + mirror__public=True, mirror__active=True, + protocol__protocol__iexact='HTTP') + # look first for an 'Any' URL, then fall back to any HTTP URL + filtered_urls = mirror_urls.filter(mirror__country='Any')[:1] + if not filtered_urls: + filtered_urls = mirror_urls[:1] + if not filtered_urls: + raise Http404 + arch = pkg.arch.name + if pkg.arch.agnostic: + # grab the first non-any arch to fake the download path + arch = Arch.objects.exclude(agnostic=True)[0].name + values = { + 'host': filtered_urls[0].url, + 'arch': arch, + 'repo': pkg.repo.name.lower(), + 'file': pkg.filename, + } + url = Template('${host}${repo}/os/${arch}/${file}').substitute(values) + return redirect(url) + +def arch_differences(request): + # TODO: we have some hardcoded magic here with respect to the arches. + arch_a = Arch.objects.get(name='i686') + arch_b = Arch.objects.get(name='x86_64') + differences = get_differences_info(arch_a, arch_b) + context = { + 'arch_a': arch_a, + 'arch_b': arch_b, + 'differences': differences, + } + return direct_to_template(request, 'packages/differences.html', context) + +@permission_required('main.change_package') +@never_cache +def stale_relations(request): + relations = PackageRelation.objects.select_related('user') + pkgbases = Package.objects.all().values('pkgbase') + + inactive_user = relations.filter(user__is_active=False) + missing_pkgbase = relations.exclude( + pkgbase__in=pkgbases).order_by('pkgbase') + wrong_permissions = get_wrong_permissions() + + context = { + 'inactive_user': inactive_user, + 'missing_pkgbase': missing_pkgbase, + 'wrong_permissions': wrong_permissions, + } + return direct_to_template(request, 'packages/stale_relations.html', context) + +@permission_required('packages.delete_packagerelation') +@require_POST +def stale_relations_update(request): + ids = set(request.POST.getlist('relation_id')) + + if ids: + PackageRelation.objects.filter(id__in=ids).delete() + + messages.info(request, "%d package relations deleted." % len(ids)) + return redirect('/packages/stale_relations/') + +# vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From a1ef52f87fb7c2fa976431393769c9c4ec88ba22 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 10 Nov 2011 18:25:25 -0600 Subject: packages/views: move flag-related views to own module One step in splitting the package views. Signed-off-by: Dan McGee --- packages/views/__init__.py | 110 ++--------------------------------------- packages/views/flag.py | 121 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 107 deletions(-) create mode 100644 packages/views/flag.py (limited to 'packages') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 7b8e4847..4782e457 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -3,15 +3,12 @@ from django.contrib.admin.widgets import AdminDateWidget from django.contrib.auth.models import User from django.contrib.auth.decorators import permission_required -from django.conf import settings -from django.core.mail import send_mail from django.core.serializers.json import DjangoJSONEncoder from django.db import transaction from django.db.models import Q from django.http import HttpResponse, Http404, HttpResponseForbidden from django.shortcuts import (get_object_or_404, get_list_or_404, redirect, render) -from django.template import loader, Context from django.utils import simplejson from django.views.decorators.cache import never_cache from django.views.decorators.http import require_POST @@ -33,6 +30,9 @@ get_wrong_permissions, get_signoff_groups, approved_by_signoffs, PackageSignoffGroup) +# make other views available from this same package +from .flag import flaghelp, flag, flag_confirmed, unflag, unflag_all + class PackageJSONEncoder(DjangoJSONEncoder): pkg_attributes = [ 'pkgname', 'pkgbase', 'repo', 'arch', 'pkgver', 'pkgrel', 'epoch', 'pkgdesc', 'url', 'filename', 'compressed_size', @@ -355,24 +355,6 @@ def files_json(request, name, repo, arch): cls=PackageJSONEncoder) return HttpResponse(to_json, mimetype='application/json') -@permission_required('main.change_package') -def unflag(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) - pkg.flag_date = None - pkg.save() - return redirect(pkg) - -@permission_required('main.change_package') -def unflag_all(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) - # find all packages from (hopefully) the same PKGBUILD - pkgs = Package.objects.filter(pkgbase=pkg.pkgbase, - repo__testing=pkg.repo.testing, repo__staging=pkg.repo.staging) - pkgs.update(flag_date=None) - return redirect(pkg) - @permission_required('main.change_package') @never_cache @@ -542,92 +524,6 @@ def signoffs_json(request): return response -def flaghelp(request): - return direct_to_template(request, 'packages/flaghelp.html') - -class FlagForm(forms.Form): - email = forms.EmailField(label='* E-mail Address') - usermessage = forms.CharField(label='Message To Dev', - widget=forms.Textarea, required=False) - # The field below is used to filter out bots that blindly fill out all input elements - website = forms.CharField(label='', - widget=forms.TextInput(attrs={'style': 'display:none;'}), - required=False) - -@never_cache -def flag(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) - if pkg.flag_date is not None: - # already flagged. do nothing. - return direct_to_template(request, 'packages/flagged.html', {'pkg': pkg}) - # find all packages from (hopefully) the same PKGBUILD - pkgs = Package.objects.normal().filter( - pkgbase=pkg.pkgbase, flag_date__isnull=True, - repo__testing=pkg.repo.testing, - repo__staging=pkg.repo.staging).order_by( - 'pkgname', 'repo__name', 'arch__name') - - if request.POST: - form = FlagForm(request.POST) - if form.is_valid() and form.cleaned_data['website'] == '': - # save the package list for later use - flagged_pkgs = list(pkgs) - pkgs.update(flag_date=datetime.utcnow()) - - maints = pkg.maintainers - if not maints: - toemail = settings.NOTIFICATIONS - subject = 'Orphan %s package [%s] marked out-of-date' % \ - (pkg.repo.name, pkg.pkgname) - else: - toemail = [] - subject = '%s package [%s] marked out-of-date' % \ - (pkg.repo.name, pkg.pkgname) - for maint in maints: - if maint.get_profile().notify == True: - toemail.append(maint.email) - - if toemail: - # send notification email to the maintainers - t = loader.get_template('packages/outofdate.txt') - c = Context({ - 'email': form.cleaned_data['email'], - 'message': form.cleaned_data['usermessage'], - 'pkg': pkg, - 'packages': flagged_pkgs, - }) - send_mail(subject, - t.render(c), - 'Arch Website Notification ', - toemail, - fail_silently=True) - - return redirect('package-flag-confirmed', name=name, repo=repo, - arch=arch) - else: - form = FlagForm() - - context = { - 'package': pkg, - 'packages': pkgs, - 'form': form - } - return direct_to_template(request, 'packages/flag.html', context) - -def flag_confirmed(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) - pkgs = Package.objects.normal().filter( - pkgbase=pkg.pkgbase, flag_date=pkg.flag_date, - repo__testing=pkg.repo.testing, - repo__staging=pkg.repo.staging).order_by( - 'pkgname', 'repo__name', 'arch__name') - - context = {'package': pkg, 'packages': pkgs} - - return direct_to_template(request, 'packages/flag_confirmed.html', context) - def download(request, name, repo, arch): pkg = get_object_or_404(Package, pkgname=name, repo__name__iexact=repo, arch__name=arch) diff --git a/packages/views/flag.py b/packages/views/flag.py new file mode 100644 index 00000000..7e9d87c7 --- /dev/null +++ b/packages/views/flag.py @@ -0,0 +1,121 @@ +from datetime import datetime + +from django import forms +from django.conf import settings +from django.contrib.auth.decorators import permission_required +from django.core.mail import send_mail +from django.shortcuts import get_object_or_404, redirect +from django.template import loader, Context +from django.views.generic.simple import direct_to_template +from django.views.decorators.cache import never_cache + +from main.models import Package + + +def flaghelp(request): + return direct_to_template(request, 'packages/flaghelp.html') + +class FlagForm(forms.Form): + email = forms.EmailField(label='* E-mail Address') + usermessage = forms.CharField(label='Message To Dev', + widget=forms.Textarea, required=False) + # The field below is used to filter out bots that blindly fill out all + # input elements + website = forms.CharField(label='', + widget=forms.TextInput(attrs={'style': 'display:none;'}), + required=False) + +@never_cache +def flag(request, name, repo, arch): + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) + if pkg.flag_date is not None: + # already flagged. do nothing. + return direct_to_template(request, 'packages/flagged.html', + {'pkg': pkg}) + # find all packages from (hopefully) the same PKGBUILD + pkgs = Package.objects.normal().filter( + pkgbase=pkg.pkgbase, flag_date__isnull=True, + repo__testing=pkg.repo.testing, + repo__staging=pkg.repo.staging).order_by( + 'pkgname', 'repo__name', 'arch__name') + + if request.POST: + form = FlagForm(request.POST) + if form.is_valid() and form.cleaned_data['website'] == '': + # save the package list for later use + flagged_pkgs = list(pkgs) + pkgs.update(flag_date=datetime.utcnow()) + + maints = pkg.maintainers + if not maints: + toemail = settings.NOTIFICATIONS + subject = 'Orphan %s package [%s] marked out-of-date' % \ + (pkg.repo.name, pkg.pkgname) + else: + toemail = [] + subject = '%s package [%s] marked out-of-date' % \ + (pkg.repo.name, pkg.pkgname) + for maint in maints: + if maint.get_profile().notify == True: + toemail.append(maint.email) + + if toemail: + # send notification email to the maintainers + tmpl = loader.get_template('packages/outofdate.txt') + ctx = Context({ + 'email': form.cleaned_data['email'], + 'message': form.cleaned_data['usermessage'], + 'pkg': pkg, + 'packages': flagged_pkgs, + }) + send_mail(subject, + tmpl.render(ctx), + 'Arch Website Notification ', + toemail, + fail_silently=True) + + return redirect('package-flag-confirmed', name=name, repo=repo, + arch=arch) + else: + form = FlagForm() + + context = { + 'package': pkg, + 'packages': pkgs, + 'form': form + } + return direct_to_template(request, 'packages/flag.html', context) + +def flag_confirmed(request, name, repo, arch): + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) + pkgs = Package.objects.normal().filter( + pkgbase=pkg.pkgbase, flag_date=pkg.flag_date, + repo__testing=pkg.repo.testing, + repo__staging=pkg.repo.staging).order_by( + 'pkgname', 'repo__name', 'arch__name') + + context = {'package': pkg, 'packages': pkgs} + + return direct_to_template(request, 'packages/flag_confirmed.html', context) + +@permission_required('main.change_package') +def unflag(request, name, repo, arch): + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) + pkg.flag_date = None + pkg.save() + return redirect(pkg) + +@permission_required('main.change_package') +def unflag_all(request, name, repo, arch): + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) + # find all packages from (hopefully) the same PKGBUILD + pkgs = Package.objects.filter(pkgbase=pkg.pkgbase, + repo__testing=pkg.repo.testing, repo__staging=pkg.repo.staging) + pkgs.update(flag_date=None) + return redirect(pkg) + +# vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 865fa5c1e34123a066d5366e04dda84f84232ade Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 10 Nov 2011 18:35:29 -0600 Subject: packages/views: move signoff-related views into separate module Signed-off-by: Dan McGee --- packages/views/__init__.py | 179 +------------------------------------------ packages/views/signoff.py | 187 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 175 deletions(-) create mode 100644 packages/views/signoff.py (limited to 'packages') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 4782e457..e02740f2 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -4,7 +4,6 @@ from django.contrib.auth.models import User from django.contrib.auth.decorators import permission_required from django.core.serializers.json import DjangoJSONEncoder -from django.db import transaction from django.db.models import Q from django.http import HttpResponse, Http404, HttpResponseForbidden from django.shortcuts import (get_object_or_404, get_list_or_404, @@ -17,21 +16,20 @@ from django.views.generic.simple import direct_to_template from datetime import datetime -from operator import attrgetter from string import Template from urllib import urlencode from main.models import Package, PackageFile, Arch, Repo from main.utils import make_choice from mirrors.models import MirrorUrl -from ..models import (PackageRelation, PackageGroup, - SignoffSpecification, Signoff) +from ..models import PackageRelation, PackageGroup from ..utils import (get_group_info, get_differences_info, - get_wrong_permissions, get_signoff_groups, approved_by_signoffs, - PackageSignoffGroup) + get_wrong_permissions) # make other views available from this same package from .flag import flaghelp, flag, flag_confirmed, unflag, unflag_all +from .signoff import signoffs, signoff_package, signoff_options, signoffs_json + class PackageJSONEncoder(DjangoJSONEncoder): pkg_attributes = [ 'pkgname', 'pkgbase', 'repo', 'arch', 'pkgver', @@ -355,175 +353,6 @@ def files_json(request, name, repo, arch): cls=PackageJSONEncoder) return HttpResponse(to_json, mimetype='application/json') - -@permission_required('main.change_package') -@never_cache -def signoffs(request): - signoff_groups = sorted(get_signoff_groups(), key=attrgetter('pkgbase')) - for group in signoff_groups: - group.user = request.user - - context = { - 'signoff_groups': signoff_groups, - 'arches': Arch.objects.all(), - 'repo_names': sorted(set(g.target_repo for g in signoff_groups)), - } - return direct_to_template(request, 'packages/signoffs.html', context) - -@permission_required('main.change_package') -@never_cache -def signoff_package(request, name, repo, arch, revoke=False): - packages = get_list_or_404(Package, pkgbase=name, - arch__name=arch, repo__name__iexact=repo, repo__testing=True) - package = packages[0] - - spec = SignoffSpecification.objects.get_or_default_from_package(package) - - if revoke: - try: - signoff = Signoff.objects.get_from_package( - package, request.user, False) - except Signoff.DoesNotExist: - raise Http404 - signoff.revoked = datetime.utcnow() - signoff.save() - created = False - else: - # ensure we should even be accepting signoffs - if spec.known_bad or not spec.enabled: - return render(request, '403.html', status=403) - signoff, created = Signoff.objects.get_or_create_from_package( - package, request.user) - - all_signoffs = Signoff.objects.for_package(package) - - if request.is_ajax(): - data = { - 'created': created, - 'revoked': bool(signoff.revoked), - 'approved': approved_by_signoffs(all_signoffs, spec), - 'required': spec.required, - 'enabled': spec.enabled, - 'known_bad': spec.known_bad, - 'user': str(request.user), - } - return HttpResponse(simplejson.dumps(data, ensure_ascii=False), - mimetype='application/json') - - return redirect('package-signoffs') - -class SignoffOptionsForm(forms.ModelForm): - apply_all = forms.BooleanField(required=False, - help_text="Apply these options to all architectures?") - - class Meta: - model = SignoffSpecification - fields = ('required', 'enabled', 'known_bad', 'comments') - -def _signoff_options_all(request, name, repo): - seen_ids = set() - with transaction.commit_on_success(): - # find or create a specification for all architectures, then - # graft the form data onto them - packages = Package.objects.filter(pkgbase=name, - repo__name__iexact=repo, repo__testing=True) - for package in packages: - try: - spec = SignoffSpecification.objects.get_from_package(package) - if spec.pk in seen_ids: - continue - except SignoffSpecification.DoesNotExist: - spec = SignoffSpecification(pkgbase=package.pkgbase, - pkgver=package.pkgver, pkgrel=package.pkgrel, - epoch=package.epoch, arch=package.arch, - repo=package.repo) - spec.user = request.user - form = SignoffOptionsForm(request.POST, instance=spec) - if form.is_valid(): - form.save() - seen_ids.add(form.instance.pk) - -@permission_required('main.change_package') -@never_cache -def signoff_options(request, name, repo, arch): - packages = get_list_or_404(Package, pkgbase=name, - arch__name=arch, repo__name__iexact=repo, repo__testing=True) - package = packages[0] - - if request.user != package.packager and \ - request.user not in package.maintainers: - return render(request, '403.html', status=403) - - try: - spec = SignoffSpecification.objects.get_from_package(package) - except SignoffSpecification.DoesNotExist: - # create a fake one, but don't save it just yet - spec = SignoffSpecification(pkgbase=package.pkgbase, - pkgver=package.pkgver, pkgrel=package.pkgrel, - epoch=package.epoch, arch=package.arch, repo=package.repo) - spec.user = request.user - - if request.POST: - form = SignoffOptionsForm(request.POST, instance=spec) - if form.is_valid(): - if form.cleaned_data['apply_all']: - _signoff_options_all(request, name, repo) - else: - form.save() - return redirect('package-signoffs') - else: - form = SignoffOptionsForm(instance=spec) - - context = { - 'packages': packages, - 'package': package, - 'form': form, - } - return direct_to_template(request, 'packages/signoff_options.html', context) - -class SignoffJSONEncoder(DjangoJSONEncoder): - '''Base JSONEncoder extended to handle all serialization of all classes - related to signoffs.''' - signoff_group_attrs = ['arch', 'last_update', 'maintainers', 'packager', - 'pkgbase', 'repo', 'signoffs', 'target_repo', 'version'] - signoff_spec_attrs = ['required', 'enabled', 'known_bad', 'comments'] - signoff_attrs = ['user', 'created', 'revoked'] - - def default(self, obj): - if isinstance(obj, PackageSignoffGroup): - data = dict((attr, getattr(obj, attr)) - for attr in self.signoff_group_attrs) - data['package_count'] = len(obj.packages) - data['approved'] = obj.approved() - data.update((attr, getattr(obj.specification, attr)) - for attr in self.signoff_spec_attrs) - return data - elif isinstance(obj, Signoff): - data = dict((attr, getattr(obj, attr)) - for attr in self.signoff_attrs) - return data - elif isinstance(obj, Arch) or isinstance(obj, Repo): - return unicode(obj) - elif isinstance(obj, User): - return obj.username - elif isinstance(obj, set): - return list(obj) - return super(SignoffJSONEncoder, self).default(obj) - -@permission_required('main.change_package') -@never_cache -def signoffs_json(request): - signoff_groups = sorted(get_signoff_groups(), key=attrgetter('pkgbase')) - data = { - 'version': 1, - 'signoff_groups': signoff_groups, - } - to_json = simplejson.dumps(data, ensure_ascii=False, - cls=SignoffJSONEncoder) - response = HttpResponse(to_json, mimetype='application/json') - return response - - def download(request, name, repo, arch): pkg = get_object_or_404(Package, pkgname=name, repo__name__iexact=repo, arch__name=arch) diff --git a/packages/views/signoff.py b/packages/views/signoff.py new file mode 100644 index 00000000..a42c1c66 --- /dev/null +++ b/packages/views/signoff.py @@ -0,0 +1,187 @@ +from datetime import datetime +from operator import attrgetter + +from django import forms +from django.contrib.auth.decorators import permission_required +from django.contrib.auth.models import User +from django.core.serializers.json import DjangoJSONEncoder +from django.db import transaction +from django.http import HttpResponse, Http404 +from django.shortcuts import get_list_or_404, redirect, render +from django.utils import simplejson +from django.views.decorators.cache import never_cache +from django.views.generic.simple import direct_to_template + +from main.models import Package, Arch, Repo +from ..models import SignoffSpecification, Signoff +from ..utils import (get_signoff_groups, approved_by_signoffs, + PackageSignoffGroup) + +@permission_required('main.change_package') +@never_cache +def signoffs(request): + signoff_groups = sorted(get_signoff_groups(), key=attrgetter('pkgbase')) + for group in signoff_groups: + group.user = request.user + + context = { + 'signoff_groups': signoff_groups, + 'arches': Arch.objects.all(), + 'repo_names': sorted(set(g.target_repo for g in signoff_groups)), + } + return direct_to_template(request, 'packages/signoffs.html', context) + +@permission_required('main.change_package') +@never_cache +def signoff_package(request, name, repo, arch, revoke=False): + packages = get_list_or_404(Package, pkgbase=name, + arch__name=arch, repo__name__iexact=repo, repo__testing=True) + package = packages[0] + + spec = SignoffSpecification.objects.get_or_default_from_package(package) + + if revoke: + try: + signoff = Signoff.objects.get_from_package( + package, request.user, False) + except Signoff.DoesNotExist: + raise Http404 + signoff.revoked = datetime.utcnow() + signoff.save() + created = False + else: + # ensure we should even be accepting signoffs + if spec.known_bad or not spec.enabled: + return render(request, '403.html', status=403) + signoff, created = Signoff.objects.get_or_create_from_package( + package, request.user) + + all_signoffs = Signoff.objects.for_package(package) + + if request.is_ajax(): + data = { + 'created': created, + 'revoked': bool(signoff.revoked), + 'approved': approved_by_signoffs(all_signoffs, spec), + 'required': spec.required, + 'enabled': spec.enabled, + 'known_bad': spec.known_bad, + 'user': str(request.user), + } + return HttpResponse(simplejson.dumps(data, ensure_ascii=False), + mimetype='application/json') + + return redirect('package-signoffs') + +class SignoffOptionsForm(forms.ModelForm): + apply_all = forms.BooleanField(required=False, + help_text="Apply these options to all architectures?") + + class Meta: + model = SignoffSpecification + fields = ('required', 'enabled', 'known_bad', 'comments') + +def _signoff_options_all(request, name, repo): + seen_ids = set() + with transaction.commit_on_success(): + # find or create a specification for all architectures, then + # graft the form data onto them + packages = Package.objects.filter(pkgbase=name, + repo__name__iexact=repo, repo__testing=True) + for package in packages: + try: + spec = SignoffSpecification.objects.get_from_package(package) + if spec.pk in seen_ids: + continue + except SignoffSpecification.DoesNotExist: + spec = SignoffSpecification(pkgbase=package.pkgbase, + pkgver=package.pkgver, pkgrel=package.pkgrel, + epoch=package.epoch, arch=package.arch, + repo=package.repo) + spec.user = request.user + form = SignoffOptionsForm(request.POST, instance=spec) + if form.is_valid(): + form.save() + seen_ids.add(form.instance.pk) + +@permission_required('main.change_package') +@never_cache +def signoff_options(request, name, repo, arch): + packages = get_list_or_404(Package, pkgbase=name, + arch__name=arch, repo__name__iexact=repo, repo__testing=True) + package = packages[0] + + if request.user != package.packager and \ + request.user not in package.maintainers: + return render(request, '403.html', status=403) + + try: + spec = SignoffSpecification.objects.get_from_package(package) + except SignoffSpecification.DoesNotExist: + # create a fake one, but don't save it just yet + spec = SignoffSpecification(pkgbase=package.pkgbase, + pkgver=package.pkgver, pkgrel=package.pkgrel, + epoch=package.epoch, arch=package.arch, repo=package.repo) + spec.user = request.user + + if request.POST: + form = SignoffOptionsForm(request.POST, instance=spec) + if form.is_valid(): + if form.cleaned_data['apply_all']: + _signoff_options_all(request, name, repo) + else: + form.save() + return redirect('package-signoffs') + else: + form = SignoffOptionsForm(instance=spec) + + context = { + 'packages': packages, + 'package': package, + 'form': form, + } + return direct_to_template(request, 'packages/signoff_options.html', context) + +class SignoffJSONEncoder(DjangoJSONEncoder): + '''Base JSONEncoder extended to handle all serialization of all classes + related to signoffs.''' + signoff_group_attrs = ['arch', 'last_update', 'maintainers', 'packager', + 'pkgbase', 'repo', 'signoffs', 'target_repo', 'version'] + signoff_spec_attrs = ['required', 'enabled', 'known_bad', 'comments'] + signoff_attrs = ['user', 'created', 'revoked'] + + def default(self, obj): + if isinstance(obj, PackageSignoffGroup): + data = dict((attr, getattr(obj, attr)) + for attr in self.signoff_group_attrs) + data['package_count'] = len(obj.packages) + data['approved'] = obj.approved() + data.update((attr, getattr(obj.specification, attr)) + for attr in self.signoff_spec_attrs) + return data + elif isinstance(obj, Signoff): + data = dict((attr, getattr(obj, attr)) + for attr in self.signoff_attrs) + return data + elif isinstance(obj, Arch) or isinstance(obj, Repo): + return unicode(obj) + elif isinstance(obj, User): + return obj.username + elif isinstance(obj, set): + return list(obj) + return super(SignoffJSONEncoder, self).default(obj) + +@permission_required('main.change_package') +@never_cache +def signoffs_json(request): + signoff_groups = sorted(get_signoff_groups(), key=attrgetter('pkgbase')) + data = { + 'version': 1, + 'signoff_groups': signoff_groups, + } + to_json = simplejson.dumps(data, ensure_ascii=False, + cls=SignoffJSONEncoder) + response = HttpResponse(to_json, mimetype='application/json') + return response + +# vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 6e6392c089688e227339efd58d42f84de92bda11 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 10 Nov 2011 19:09:29 -0600 Subject: packages/views: split out search view Signed-off-by: Dan McGee --- packages/views/__init__.py | 160 +------------------------------------------- packages/views/search.py | 161 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 157 deletions(-) create mode 100644 packages/views/search.py (limited to 'packages') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index e02740f2..e3264161 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -1,26 +1,18 @@ -from django import forms from django.contrib import messages -from django.contrib.admin.widgets import AdminDateWidget -from django.contrib.auth.models import User from django.contrib.auth.decorators import permission_required from django.core.serializers.json import DjangoJSONEncoder -from django.db.models import Q -from django.http import HttpResponse, Http404, HttpResponseForbidden -from django.shortcuts import (get_object_or_404, get_list_or_404, - redirect, render) +from django.http import HttpResponse, Http404 +from django.shortcuts import get_object_or_404, redirect from django.utils import simplejson from django.views.decorators.cache import never_cache from django.views.decorators.http import require_POST from django.views.decorators.vary import vary_on_headers -from django.views.generic import list_detail from django.views.generic.simple import direct_to_template -from datetime import datetime from string import Template from urllib import urlencode from main.models import Package, PackageFile, Arch, Repo -from main.utils import make_choice from mirrors.models import MirrorUrl from ..models import PackageRelation, PackageGroup from ..utils import (get_group_info, get_differences_info, @@ -28,6 +20,7 @@ # make other views available from this same package from .flag import flaghelp, flag, flag_confirmed, unflag, unflag_all +from .search import search from .signoff import signoffs, signoff_package, signoff_options, signoffs_json @@ -171,153 +164,6 @@ def group_details(request, arch, name): } return direct_to_template(request, 'packages/packages_list.html', context) -def coerce_limit_value(value): - if not value: - return None - if value == 'all': - # negative value indicates show all results - return -1 - value = int(value) - if value < 0: - raise ValueError - return value - -class LimitTypedChoiceField(forms.TypedChoiceField): - def valid_value(self, value): - try: - coerce_limit_value(value) - return True - except (ValueError, TypeError): - return False - -class PackageSearchForm(forms.Form): - repo = forms.MultipleChoiceField(required=False) - arch = forms.MultipleChoiceField(required=False) - name = forms.CharField(required=False) - desc = forms.CharField(required=False) - q = forms.CharField(required=False) - maintainer = forms.ChoiceField(required=False) - packager = forms.ChoiceField(required=False) - last_update = forms.DateField(required=False, widget=AdminDateWidget(), - label='Last Updated After') - flagged = forms.ChoiceField( - choices=[('', 'All')] + make_choice(['Flagged', 'Not Flagged']), - required=False) - signed = forms.ChoiceField( - choices=[('', 'All')] + make_choice(['Signed', 'Unsigned']), - required=False) - limit = LimitTypedChoiceField( - choices=make_choice([50, 100, 250]) + [('all', 'All')], - coerce=coerce_limit_value, - required=False, - initial=50) - - def __init__(self, *args, **kwargs): - super(PackageSearchForm, self).__init__(*args, **kwargs) - self.fields['repo'].choices = make_choice( - [repo.name for repo in Repo.objects.all()]) - self.fields['arch'].choices = make_choice( - [arch.name for arch in Arch.objects.all()]) - self.fields['q'].widget.attrs.update({"size": "30"}) - maints = User.objects.filter(is_active=True).order_by('username') - self.fields['maintainer'].choices = \ - [('', 'All'), ('orphan', 'Orphan')] + \ - [(m.username, m.get_full_name()) for m in maints] - self.fields['packager'].choices = \ - [('', 'All'), ('unknown', 'Unknown')] + \ - [(m.username, m.get_full_name()) for m in maints] - -def search(request, page=None): - limit = 50 - packages = Package.objects.normal() - - if request.GET: - form = PackageSearchForm(data=request.GET) - if form.is_valid(): - if form.cleaned_data['repo']: - packages = packages.filter( - repo__name__in=form.cleaned_data['repo']) - - if form.cleaned_data['arch']: - packages = packages.filter( - arch__name__in=form.cleaned_data['arch']) - - if form.cleaned_data['maintainer'] == 'orphan': - inner_q = PackageRelation.objects.all().values('pkgbase') - packages = packages.exclude(pkgbase__in=inner_q) - elif form.cleaned_data['maintainer']: - inner_q = PackageRelation.objects.filter( - user__username=form.cleaned_data['maintainer']).values('pkgbase') - packages = packages.filter(pkgbase__in=inner_q) - - if form.cleaned_data['packager'] == 'unknown': - packages = packages.filter(packager__isnull=True) - elif form.cleaned_data['packager']: - packages = packages.filter( - packager__username=form.cleaned_data['packager']) - - if form.cleaned_data['flagged'] == 'Flagged': - packages = packages.filter(flag_date__isnull=False) - elif form.cleaned_data['flagged'] == 'Not Flagged': - packages = packages.filter(flag_date__isnull=True) - - if form.cleaned_data['signed'] == 'Signed': - packages = packages.filter(pgp_signature__isnull=False) - elif form.cleaned_data['signed'] == 'Unsigned': - packages = packages.filter(pgp_signature__isnull=True) - - if form.cleaned_data['last_update']: - lu = form.cleaned_data['last_update'] - packages = packages.filter(last_update__gte= - datetime(lu.year, lu.month, lu.day, 0, 0)) - - if form.cleaned_data['name']: - name = form.cleaned_data['name'] - packages = packages.filter(pkgname__icontains=name) - - if form.cleaned_data['desc']: - desc = form.cleaned_data['desc'] - packages = packages.filter(pkgdesc__icontains=desc) - - if form.cleaned_data['q']: - query = form.cleaned_data['q'] - q = Q(pkgname__icontains=query) | Q(pkgdesc__icontains=query) - packages = packages.filter(q) - - asked_limit = form.cleaned_data['limit'] - if asked_limit and asked_limit < 0: - limit = None - elif asked_limit: - limit = asked_limit - else: - # Form had errors, don't return any results, just the busted form - packages = Package.objects.none() - else: - form = PackageSearchForm() - - current_query = request.GET.urlencode() - page_dict = { - 'search_form': form, - 'current_query': current_query - } - allowed_sort = ["arch", "repo", "pkgname", "pkgbase", - "compressed_size", "installed_size", - "build_date", "last_update", "flag_date"] - allowed_sort += ["-" + s for s in allowed_sort] - sort = request.GET.get('sort', None) - if sort in allowed_sort: - packages = packages.order_by(sort) - page_dict['sort'] = sort - else: - packages = packages.order_by('pkgname') - - return list_detail.object_list(request, packages, - template_name="packages/search.html", - page=page, - paginate_by=limit, - template_object_name="package", - extra_context=page_dict) - @vary_on_headers('X-Requested-With') def files(request, name, repo, arch): pkg = get_object_or_404(Package, diff --git a/packages/views/search.py b/packages/views/search.py new file mode 100644 index 00000000..e2d00d62 --- /dev/null +++ b/packages/views/search.py @@ -0,0 +1,161 @@ +from datetime import datetime + +from django import forms +from django.contrib.admin.widgets import AdminDateWidget +from django.contrib.auth.models import User +from django.db.models import Q +from django.views.generic import list_detail + +from main.models import Package, Arch, Repo +from main.utils import make_choice +from ..models import PackageRelation + + +def coerce_limit_value(value): + if not value: + return None + if value == 'all': + # negative value indicates show all results + return -1 + value = int(value) + if value < 0: + raise ValueError + return value + +class LimitTypedChoiceField(forms.TypedChoiceField): + def valid_value(self, value): + try: + coerce_limit_value(value) + return True + except (ValueError, TypeError): + return False + +class PackageSearchForm(forms.Form): + repo = forms.MultipleChoiceField(required=False) + arch = forms.MultipleChoiceField(required=False) + name = forms.CharField(required=False) + desc = forms.CharField(required=False) + q = forms.CharField(required=False) + maintainer = forms.ChoiceField(required=False) + packager = forms.ChoiceField(required=False) + last_update = forms.DateField(required=False, widget=AdminDateWidget(), + label='Last Updated After') + flagged = forms.ChoiceField( + choices=[('', 'All')] + make_choice(['Flagged', 'Not Flagged']), + required=False) + signed = forms.ChoiceField( + choices=[('', 'All')] + make_choice(['Signed', 'Unsigned']), + required=False) + limit = LimitTypedChoiceField( + choices=make_choice([50, 100, 250]) + [('all', 'All')], + coerce=coerce_limit_value, + required=False, + initial=50) + + def __init__(self, *args, **kwargs): + super(PackageSearchForm, self).__init__(*args, **kwargs) + self.fields['repo'].choices = make_choice( + [repo.name for repo in Repo.objects.all()]) + self.fields['arch'].choices = make_choice( + [arch.name for arch in Arch.objects.all()]) + self.fields['q'].widget.attrs.update({"size": "30"}) + maints = User.objects.filter(is_active=True).order_by('username') + self.fields['maintainer'].choices = \ + [('', 'All'), ('orphan', 'Orphan')] + \ + [(m.username, m.get_full_name()) for m in maints] + self.fields['packager'].choices = \ + [('', 'All'), ('unknown', 'Unknown')] + \ + [(m.username, m.get_full_name()) for m in maints] + +def search(request, page=None): + limit = 50 + packages = Package.objects.normal() + + if request.GET: + form = PackageSearchForm(data=request.GET) + if form.is_valid(): + if form.cleaned_data['repo']: + packages = packages.filter( + repo__name__in=form.cleaned_data['repo']) + + if form.cleaned_data['arch']: + packages = packages.filter( + arch__name__in=form.cleaned_data['arch']) + + if form.cleaned_data['maintainer'] == 'orphan': + inner_q = PackageRelation.objects.all().values('pkgbase') + packages = packages.exclude(pkgbase__in=inner_q) + elif form.cleaned_data['maintainer']: + inner_q = PackageRelation.objects.filter( + user__username=form.cleaned_data['maintainer']).values('pkgbase') + packages = packages.filter(pkgbase__in=inner_q) + + if form.cleaned_data['packager'] == 'unknown': + packages = packages.filter(packager__isnull=True) + elif form.cleaned_data['packager']: + packages = packages.filter( + packager__username=form.cleaned_data['packager']) + + if form.cleaned_data['flagged'] == 'Flagged': + packages = packages.filter(flag_date__isnull=False) + elif form.cleaned_data['flagged'] == 'Not Flagged': + packages = packages.filter(flag_date__isnull=True) + + if form.cleaned_data['signed'] == 'Signed': + packages = packages.filter(pgp_signature__isnull=False) + elif form.cleaned_data['signed'] == 'Unsigned': + packages = packages.filter(pgp_signature__isnull=True) + + if form.cleaned_data['last_update']: + lu = form.cleaned_data['last_update'] + packages = packages.filter(last_update__gte= + datetime(lu.year, lu.month, lu.day, 0, 0)) + + if form.cleaned_data['name']: + name = form.cleaned_data['name'] + packages = packages.filter(pkgname__icontains=name) + + if form.cleaned_data['desc']: + desc = form.cleaned_data['desc'] + packages = packages.filter(pkgdesc__icontains=desc) + + if form.cleaned_data['q']: + query = form.cleaned_data['q'] + q = Q(pkgname__icontains=query) | Q(pkgdesc__icontains=query) + packages = packages.filter(q) + + asked_limit = form.cleaned_data['limit'] + if asked_limit and asked_limit < 0: + limit = None + elif asked_limit: + limit = asked_limit + else: + # Form had errors, don't return any results, just the busted form + packages = Package.objects.none() + else: + form = PackageSearchForm() + + current_query = request.GET.urlencode() + page_dict = { + 'search_form': form, + 'current_query': current_query + } + allowed_sort = ["arch", "repo", "pkgname", "pkgbase", + "compressed_size", "installed_size", + "build_date", "last_update", "flag_date"] + allowed_sort += ["-" + s for s in allowed_sort] + sort = request.GET.get('sort', None) + if sort in allowed_sort: + packages = packages.order_by(sort) + page_dict['sort'] = sort + else: + packages = packages.order_by('pkgname') + + return list_detail.object_list(request, packages, + template_name="packages/search.html", + page=page, + paginate_by=limit, + template_object_name="package", + extra_context=page_dict) + +# vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 504a8cabfc84b4ecd4fa72ddee288412dfdb7cc3 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 10 Nov 2011 19:21:44 -0600 Subject: packages/view/search: refactor out the form parsing code This is a block of very repetitive code that lends itself well to being a separate method. It would still be nice to find a way to clean this up but that can come later. Signed-off-by: Dan McGee --- packages/views/search.py | 108 +++++++++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 51 deletions(-) (limited to 'packages') diff --git a/packages/views/search.py b/packages/views/search.py index e2d00d62..57481614 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -36,6 +36,7 @@ class PackageSearchForm(forms.Form): name = forms.CharField(required=False) desc = forms.CharField(required=False) q = forms.CharField(required=False) + sort = forms.CharField(required=False) maintainer = forms.ChoiceField(required=False) packager = forms.ChoiceField(required=False) last_update = forms.DateField(required=False, widget=AdminDateWidget(), @@ -67,68 +68,74 @@ def __init__(self, *args, **kwargs): [('', 'All'), ('unknown', 'Unknown')] + \ [(m.username, m.get_full_name()) for m in maints] +def parse_form(form, packages): + if form.cleaned_data['repo']: + packages = packages.filter( + repo__name__in=form.cleaned_data['repo']) + + if form.cleaned_data['arch']: + packages = packages.filter( + arch__name__in=form.cleaned_data['arch']) + + if form.cleaned_data['maintainer'] == 'orphan': + inner_q = PackageRelation.objects.all().values('pkgbase') + packages = packages.exclude(pkgbase__in=inner_q) + elif form.cleaned_data['maintainer']: + inner_q = PackageRelation.objects.filter( + user__username=form.cleaned_data['maintainer']).values('pkgbase') + packages = packages.filter(pkgbase__in=inner_q) + + if form.cleaned_data['packager'] == 'unknown': + packages = packages.filter(packager__isnull=True) + elif form.cleaned_data['packager']: + packages = packages.filter( + packager__username=form.cleaned_data['packager']) + + if form.cleaned_data['flagged'] == 'Flagged': + packages = packages.filter(flag_date__isnull=False) + elif form.cleaned_data['flagged'] == 'Not Flagged': + packages = packages.filter(flag_date__isnull=True) + + if form.cleaned_data['signed'] == 'Signed': + packages = packages.filter(pgp_signature__isnull=False) + elif form.cleaned_data['signed'] == 'Unsigned': + packages = packages.filter(pgp_signature__isnull=True) + + if form.cleaned_data['last_update']: + lu = form.cleaned_data['last_update'] + packages = packages.filter(last_update__gte= + datetime(lu.year, lu.month, lu.day, 0, 0)) + + if form.cleaned_data['name']: + name = form.cleaned_data['name'] + packages = packages.filter(pkgname__icontains=name) + + if form.cleaned_data['desc']: + desc = form.cleaned_data['desc'] + packages = packages.filter(pkgdesc__icontains=desc) + + if form.cleaned_data['q']: + query = form.cleaned_data['q'] + q = Q(pkgname__icontains=query) | Q(pkgdesc__icontains=query) + packages = packages.filter(q) + + return packages + def search(request, page=None): limit = 50 + sort = None packages = Package.objects.normal() if request.GET: form = PackageSearchForm(data=request.GET) if form.is_valid(): - if form.cleaned_data['repo']: - packages = packages.filter( - repo__name__in=form.cleaned_data['repo']) - - if form.cleaned_data['arch']: - packages = packages.filter( - arch__name__in=form.cleaned_data['arch']) - - if form.cleaned_data['maintainer'] == 'orphan': - inner_q = PackageRelation.objects.all().values('pkgbase') - packages = packages.exclude(pkgbase__in=inner_q) - elif form.cleaned_data['maintainer']: - inner_q = PackageRelation.objects.filter( - user__username=form.cleaned_data['maintainer']).values('pkgbase') - packages = packages.filter(pkgbase__in=inner_q) - - if form.cleaned_data['packager'] == 'unknown': - packages = packages.filter(packager__isnull=True) - elif form.cleaned_data['packager']: - packages = packages.filter( - packager__username=form.cleaned_data['packager']) - - if form.cleaned_data['flagged'] == 'Flagged': - packages = packages.filter(flag_date__isnull=False) - elif form.cleaned_data['flagged'] == 'Not Flagged': - packages = packages.filter(flag_date__isnull=True) - - if form.cleaned_data['signed'] == 'Signed': - packages = packages.filter(pgp_signature__isnull=False) - elif form.cleaned_data['signed'] == 'Unsigned': - packages = packages.filter(pgp_signature__isnull=True) - - if form.cleaned_data['last_update']: - lu = form.cleaned_data['last_update'] - packages = packages.filter(last_update__gte= - datetime(lu.year, lu.month, lu.day, 0, 0)) - - if form.cleaned_data['name']: - name = form.cleaned_data['name'] - packages = packages.filter(pkgname__icontains=name) - - if form.cleaned_data['desc']: - desc = form.cleaned_data['desc'] - packages = packages.filter(pkgdesc__icontains=desc) - - if form.cleaned_data['q']: - query = form.cleaned_data['q'] - q = Q(pkgname__icontains=query) | Q(pkgdesc__icontains=query) - packages = packages.filter(q) - + packages = parse_form(form, packages) asked_limit = form.cleaned_data['limit'] if asked_limit and asked_limit < 0: limit = None elif asked_limit: limit = asked_limit + sort = form.cleaned_data['sort'] else: # Form had errors, don't return any results, just the busted form packages = Package.objects.none() @@ -144,7 +151,6 @@ def search(request, page=None): "compressed_size", "installed_size", "build_date", "last_update", "flag_date"] allowed_sort += ["-" + s for s in allowed_sort] - sort = request.GET.get('sort', None) if sort in allowed_sort: packages = packages.order_by(sort) page_dict['sort'] = sort -- cgit v1.2.3-54-g00ecf From 626dd1156dd2b161d90ceb6bf9a9120d982c29f9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 11 Nov 2011 10:12:53 -0600 Subject: Add pkgnames array to JSON package signoffs view This makes it easier to match up exact packages with their signoff entry. Signed-off-by: Dan McGee --- packages/views/signoff.py | 1 + 1 file changed, 1 insertion(+) (limited to 'packages') diff --git a/packages/views/signoff.py b/packages/views/signoff.py index a42c1c66..26b6e710 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -154,6 +154,7 @@ def default(self, obj): if isinstance(obj, PackageSignoffGroup): data = dict((attr, getattr(obj, attr)) for attr in self.signoff_group_attrs) + data['pkgnames'] = [p.pkgname for p in obj.packages] data['package_count'] = len(obj.packages) data['approved'] = obj.approved() data.update((attr, getattr(obj.specification, attr)) -- cgit v1.2.3-54-g00ecf From 022692b3f33de8c45741d3cb27fa95f9f6facdea Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 11 Nov 2011 10:43:18 -0600 Subject: Show relevant signoffs on dashboard Signed-off-by: Dan McGee --- devel/views.py | 5 +++++ packages/utils.py | 7 ++++++- templates/devel/index.html | 50 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 3 deletions(-) (limited to 'packages') diff --git a/devel/views.py b/devel/views.py index 694ed6dc..0002df04 100644 --- a/devel/views.py +++ b/devel/views.py @@ -18,6 +18,7 @@ from main.models import Arch, Repo from main.models import UserProfile from packages.models import PackageRelation +from packages.utils import get_signoff_groups from todolists.utils import get_annotated_todolists from .utils import get_annotated_maintainers @@ -48,6 +49,9 @@ def index(request): todolists = get_annotated_todolists() todolists = [todolist for todolist in todolists if todolist.incomplete_count > 0] + signoffs = sorted(get_signoff_groups(user=request.user), + key=operator.attrgetter('pkgbase')) + maintainers = get_annotated_maintainers() maintained = PackageRelation.objects.filter( @@ -70,6 +74,7 @@ def index(request): 'orphan': orphan, 'flagged' : flagged, 'todopkgs' : todopkgs, + 'signoffs': signoffs } return direct_to_template(request, 'devel/index.html', page_dict) diff --git a/packages/utils.py b/packages/utils.py index b21ac557..0df0e382 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -330,7 +330,7 @@ def get_target_repo_map(repos): cursor.execute(sql, params) return dict(cursor.fetchall()) -def get_signoff_groups(repos=None): +def get_signoff_groups(repos=None, user=None): if repos is None: repos = Repo.objects.filter(testing=True) repo_ids = [r.pk for r in repos] @@ -340,6 +340,11 @@ def get_signoff_groups(repos=None): packages = test_pkgs.order_by('pkgname') packages = attach_maintainers(packages) + # Filter by user if asked to do so + if user is not None: + packages = [p for p in packages if user == p.packager + or user in p.maintainers] + # Collect all pkgbase values in testing repos pkgtorepo = get_target_repo_map(repos) diff --git a/templates/devel/index.html b/templates/devel/index.html index d3f7ec3b..06cf10ab 100644 --- a/templates/devel/index.html +++ b/templates/devel/index.html @@ -15,8 +15,8 @@

    My Flagged Packages

    Name - Repo Version + Repo Arch Flagged Last Updated @@ -26,8 +26,8 @@

    My Flagged Packages

    {% for pkg in flagged %} {% pkg_details_link pkg %} - {{ pkg.repo.name }} {{ pkg.full_version }} + {{ pkg.repo.name }} {{ pkg.arch.name }} {{ pkg.flag_date|date }} {{ pkg.last_update|date }} @@ -96,6 +96,47 @@

    Package Todo Lists

    +

    Signoff Status

    + + + + + + + + + + + + + + + {% for group in signoffs %} + + + + + + + {% if group.specification.known_bad %} + + {% else %} + {% if not group.specification.enabled %} + + {% else %} + + {% endif %} + {% endif %} + + + {% endfor %} + +
    NameVersionArchTarget RepoLast UpdatedApprovedSignoffs
    {% pkg_details_link group.package %}{{ group.version }}{{ group.arch.name }}{{ group.target_repo }}{{ group.last_update|date }}BadDisabled{{ group.approved|yesno|capfirst }}
      + {% for signoff in group.signoffs %} +
    • {{ signoff.user }}{% if signoff.revoked %} (revoked){% endif %}
    • + {% endfor %} +
    +

    Developer Reports

    • Big: @@ -255,6 +296,11 @@

      Stats by Developer

      {widgets: ['zebra'], sortList: [[0,0], [1,0]]}); $("#dash-todo:not(:has(tbody tr.empty))").tablesorter( {widgets: ['zebra'], sortList: [[1,1]]}); + $("#dash-signoffs:not(:has(tbody tr.empty))").tablesorter({ + widgets: ['zebra'], + sortList: [[0,0]], + headers: { 6: {sorter: false } } + }); $(".dash-stats").tablesorter({ widgets: ['zebra'], sortList: [[0,0]], -- cgit v1.2.3-54-g00ecf From a883b0af23143364ab0724fda2ecdef9aba8191f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 11 Nov 2011 11:57:04 -0600 Subject: Add a split packages sitemap With very low priority, but this should at least give a few more cross-linking pages to any crawlers using sitemaps. Signed-off-by: Dan McGee --- packages/utils.py | 18 ++++++++++++++++-- sitemaps.py | 17 ++++++++++++++++- urls.py | 1 + 3 files changed, 33 insertions(+), 3 deletions(-) (limited to 'packages') diff --git a/packages/utils.py b/packages/utils.py index 0df0e382..f8e1f2a1 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -2,10 +2,10 @@ from operator import itemgetter from django.db import connection -from django.db.models import Count, Max +from django.db.models import Count, Max, F from django.contrib.auth.models import User -from main.models import Package, Repo +from main.models import Package, Arch, Repo from main.utils import cache_function, groupby_preserve_order, PackageStandin from .models import (PackageGroup, PackageRelation, SignoffSpecification, Signoff, DEFAULT_SIGNOFF_SPEC) @@ -49,6 +49,20 @@ def get_group_info(include_arches=None): groups.extend(val.itervalues()) return sorted(groups, key=itemgetter('name', 'arch')) +def get_split_packages_info(): + '''Return info on split packages that do not have an actual package name + matching the split pkgbase.''' + pkgnames = Package.objects.values('pkgname') + split_pkgs = Package.objects.exclude(pkgname=F('pkgbase')).exclude( + pkgbase__in=pkgnames).values('pkgbase', 'repo', 'arch').annotate( + last_update=Max('last_update')) + all_arches = Arch.objects.in_bulk(set(s['arch'] for s in split_pkgs)) + all_repos = Repo.objects.in_bulk(set(s['repo'] for s in split_pkgs)) + for split in split_pkgs: + split['arch'] = all_arches[split['arch']] + split['repo'] = all_repos[split['repo']] + return split_pkgs + class Difference(object): def __init__(self, pkgname, repo, pkg_a, pkg_b): self.pkgname = pkgname diff --git a/sitemaps.py b/sitemaps.py index 8ac5bc4f..7718002d 100644 --- a/sitemaps.py +++ b/sitemaps.py @@ -3,7 +3,7 @@ from main.models import Package from news.models import News -from packages.utils import get_group_info +from packages.utils import get_group_info, get_split_packages_info class PackagesSitemap(Sitemap): changefreq = "weekly" @@ -41,6 +41,21 @@ def location(self, obj): return '/groups/%s/%s/' % (obj['arch'], obj['name']) +class SplitPackagesSitemap(Sitemap): + changefreq = "weekly" + priority = "0.3" + + def items(self): + return get_split_packages_info() + + def lastmod(self, obj): + return obj['last_update'] + + def location(self, obj): + return '/packages/%s/%s/%s/' % ( + obj['repo'].name.lower(), obj['arch'], obj['pkgbase']) + + class NewsSitemap(Sitemap): changefreq = "never" priority = "0.8" diff --git a/urls.py b/urls.py index adbc8870..1d06f0f2 100644 --- a/urls.py +++ b/urls.py @@ -18,6 +18,7 @@ 'packages': sitemaps.PackagesSitemap, 'package-files': sitemaps.PackageFilesSitemap, 'package-groups': sitemaps.PackageGroupsSitemap, + 'split-packages': sitemaps.SplitPackagesSitemap, } admin.autodiscover() -- cgit v1.2.3-54-g00ecf From 12408702eaf89ea338670ba808da9ef49e35c562 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 14 Nov 2011 12:19:17 -0600 Subject: Allow population of signoff specs with SVN commit messages This pulls them from the latest SVN commit on trunk. We don't have a failproof method of getting the exact right commit, but this should be close if it is run on a regular basis via cron (aka hourly). Note that running locally, I needed the development version of South to get the migration included here to apply because of information_schema changes in the current version of MySQL. Signed-off-by: Dan McGee --- packages/management/commands/populate_signoffs.py | 89 +++++++++++ packages/management/commands/signoff_report.py | 2 +- ...11_auto__chg_field_signoffspecification_user.py | 165 +++++++++++++++++++++ packages/models.py | 2 +- packages/views/signoff.py | 5 + settings.py | 4 + templates/packages/signoffs.html | 2 +- 7 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 packages/management/commands/populate_signoffs.py create mode 100644 packages/migrations/0011_auto__chg_field_signoffspecification_user.py (limited to 'packages') diff --git a/packages/management/commands/populate_signoffs.py b/packages/management/commands/populate_signoffs.py new file mode 100644 index 00000000..5b5acbaf --- /dev/null +++ b/packages/management/commands/populate_signoffs.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +populate_signoffs command + +Pull the latest commit message from SVN for a given package that is +signoff-eligible and does not have an existing comment attached. + +Usage: ./manage.py populate_signoffs +""" + +from datetime import datetime +import logging +import subprocess +import sys +from xml.etree.ElementTree import XML + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.management.base import NoArgsCommand + +from ...models import SignoffSpecification +from ...utils import get_signoff_groups + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s -> %(levelname)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + stream=sys.stderr) +logger = logging.getLogger() + +class Command(NoArgsCommand): + help = "Pull the latest commit message from SVN for a given package that is signoff-eligible and does not have an existing comment attached" + + def handle_noargs(self, **options): + v = int(options.get('verbosity', None)) + if v == 0: + logger.level = logging.ERROR + elif v == 1: + logger.level = logging.INFO + elif v == 2: + logger.level = logging.DEBUG + + return add_signoff_comments() + +def svn_log(pkgbase, repo): + path = '%s%s/%s/trunk/' % (settings.SVN_BASE_URL, repo.svn_root, pkgbase) + cmd = ['svn', 'log', '--limit=1', '--xml', path] + log_data = subprocess.check_output(cmd) + # the XML format is very very simple, especially with only one revision + xml = XML(log_data) + revision = int(xml.find('logentry').get('revision')) + date = datetime.strptime(xml.findtext('logentry/date'), + '%Y-%m-%dT%H:%M:%S.%fZ') + return { + 'revision': revision, + 'date': date, + 'author': xml.findtext('logentry/author'), + 'message': xml.findtext('logentry/msg'), + } + +def create_specification(package, log): + trimmed_message = log['message'].strip() + spec = SignoffSpecification(pkgbase=package.pkgbase, + pkgver=package.pkgver, pkgrel=package.pkgrel, + epoch=package.epoch, arch=package.arch, repo=package.repo, + comments=trimmed_message) + try: + spec.user = User.objects.get(username=log['author']) + except User.DoesNotExist: + pass + + return spec + +def add_signoff_comments(): + logger.info("getting all signoff groups") + groups = get_signoff_groups() + logger.info("%d signoff groups found", len(groups)) + + for group in groups: + if not group.default_spec: + continue + + logger.debug("getting SVN log for %s (%s)", group.pkgbase, group.repo) + log = svn_log(group.pkgbase, group.repo) + logger.info("creating spec with SVN message for %s", group.pkgbase) + spec = create_specification(group.packages[0], log) + spec.save() + +# vim: set ts=4 sw=4 et: diff --git a/packages/management/commands/signoff_report.py b/packages/management/commands/signoff_report.py index 3357bc1e..3b67f518 100644 --- a/packages/management/commands/signoff_report.py +++ b/packages/management/commands/signoff_report.py @@ -22,7 +22,7 @@ from operator import attrgetter import sys -from main.models import Package, Repo +from main.models import Repo from packages.models import Signoff from packages.utils import get_signoff_groups diff --git a/packages/migrations/0011_auto__chg_field_signoffspecification_user.py b/packages/migrations/0011_auto__chg_field_signoffspecification_user.py new file mode 100644 index 00000000..f6e3cdd9 --- /dev/null +++ b/packages/migrations/0011_auto__chg_field_signoffspecification_user.py @@ -0,0 +1,165 @@ +# encoding: utf-8 +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.alter_column('packages_signoffspecification', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True)) + + def backwards(self, orm): + db.alter_column('packages_signoffspecification', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['auth.User'])) + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'main.arch': { + 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"}, + 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'main.package': { + 'Meta': {'ordering': "('pkgname',)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.models.PositiveBigIntegerField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'files_last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'flag_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'installed_size': ('main.models.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Repo']"}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) + }, + 'main.repo': { + 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"}, + 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'staging': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'svn_root': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'testing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'packages.conflict': { + 'Meta': {'ordering': "['name']", 'object_name': 'Conflict'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'conflicts'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.license': { + 'Meta': {'ordering': "['name']", 'object_name': 'License'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'licenses'", 'to': "orm['main.Package']"}) + }, + 'packages.packagegroup': { + 'Meta': {'object_name': 'PackageGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Package']"}) + }, + 'packages.packagerelation': { + 'Meta': {'unique_together': "(('pkgbase', 'user', 'type'),)", 'object_name': 'PackageRelation'}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_relations'", 'to': "orm['auth.User']"}) + }, + 'packages.provision': { + 'Meta': {'ordering': "['name']", 'object_name': 'Provision'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'provides'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.replacement': { + 'Meta': {'ordering': "['name']", 'object_name': 'Replacement'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'replaces'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index b70c21bf..0d02ab31 100644 --- a/packages/models.py +++ b/packages/models.py @@ -73,7 +73,7 @@ class SignoffSpecification(models.Model): epoch = models.PositiveIntegerField(default=0) arch = models.ForeignKey('main.Arch') repo = models.ForeignKey('main.Repo') - user = models.ForeignKey(User) + user = models.ForeignKey(User, null=True) created = models.DateTimeField(editable=False) required = models.PositiveIntegerField(default=2, help_text="How many signoffs are required for this package?") diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 26b6e710..e57b4d9a 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -98,7 +98,10 @@ def _signoff_options_all(request, name, repo): pkgver=package.pkgver, pkgrel=package.pkgrel, epoch=package.epoch, arch=package.arch, repo=package.repo) + + if spec.user is None: spec.user = request.user + form = SignoffOptionsForm(request.POST, instance=spec) if form.is_valid(): form.save() @@ -122,6 +125,8 @@ def signoff_options(request, name, repo, arch): spec = SignoffSpecification(pkgbase=package.pkgbase, pkgver=package.pkgver, pkgrel=package.pkgrel, epoch=package.epoch, arch=package.arch, repo=package.repo) + + if spec.user is None: spec.user = request.user if request.POST: diff --git a/settings.py b/settings.py index 51f9fcf6..80e024af 100644 --- a/settings.py +++ b/settings.py @@ -134,4 +134,8 @@ # URL to fetch a current list of available ISOs ISO_LIST_URL = 'http://releng.archlinux.org/isos/' +# URL for SVN access for fetching commit messages (note absence of packages or +# community bit on the end, repo.svn_root is appended) +SVN_BASE_URL = 'svn+ssh://svn.archlinux.org/srv/svn-' + # vim: set ts=4 sw=4 et: diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index f4511f75..bd84289c 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -71,7 +71,7 @@

      Filter Displayed Signoffs

      {% if spec.required != 2 %}Required signoffs: {{ spec.required }}
      {% endif %} {% if not spec.enabled %}Signoffs are not currently enabled
      {% endif %} {% if spec.known_bad %}Package is known to be bad
      {% endif %} - {{ spec.comments|default:""|linebreaks }} + {{ spec.comments|default:""|linebreaksbr }} {% endwith %}{% endif %} {% endfor %} -- cgit v1.2.3-54-g00ecf From 0344f8ad564644c50203985255fab1d053aed463 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 15 Nov 2011 15:04:33 -0600 Subject: Add ability to cache users by username on the UserFinder This is very useful in the signoff message population script where we are very likely to encounter the same users over and over. Signed-off-by: Dan McGee --- devel/utils.py | 16 ++++++++++++++++ packages/management/commands/populate_signoffs.py | 13 ++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) (limited to 'packages') diff --git a/devel/utils.py b/devel/utils.py index d7a154a8..62b12cd5 100644 --- a/devel/utils.py +++ b/devel/utils.py @@ -47,6 +47,7 @@ def get_annotated_maintainers(): class UserFinder(object): def __init__(self): self.cache = {} + self.username_cache = {} @staticmethod def user_email(name, email): @@ -111,7 +112,22 @@ def find(self, userstring): self.cache[userstring] = user return user + def find_by_username(self, username): + if not username: + return None + if username in self.username_cache: + return self.username_cache[username] + + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + user = None + + self.username_cache[username] = user + return user + def clear_cache(self): self.cache = {} + self.username_cache = {} # vim: set ts=4 sw=4 et: diff --git a/packages/management/commands/populate_signoffs.py b/packages/management/commands/populate_signoffs.py index 5b5acbaf..ce5ec734 100644 --- a/packages/management/commands/populate_signoffs.py +++ b/packages/management/commands/populate_signoffs.py @@ -20,6 +20,7 @@ from ...models import SignoffSpecification from ...utils import get_signoff_groups +from devel.utils import UserFinder logging.basicConfig( level=logging.INFO, @@ -58,17 +59,13 @@ def svn_log(pkgbase, repo): 'message': xml.findtext('logentry/msg'), } -def create_specification(package, log): +def create_specification(package, log, finder): trimmed_message = log['message'].strip() spec = SignoffSpecification(pkgbase=package.pkgbase, pkgver=package.pkgver, pkgrel=package.pkgrel, epoch=package.epoch, arch=package.arch, repo=package.repo, comments=trimmed_message) - try: - spec.user = User.objects.get(username=log['author']) - except User.DoesNotExist: - pass - + spec.user = finder.find_by_username(log['author']) return spec def add_signoff_comments(): @@ -76,6 +73,8 @@ def add_signoff_comments(): groups = get_signoff_groups() logger.info("%d signoff groups found", len(groups)) + finder = UserFinder() + for group in groups: if not group.default_spec: continue @@ -83,7 +82,7 @@ def add_signoff_comments(): logger.debug("getting SVN log for %s (%s)", group.pkgbase, group.repo) log = svn_log(group.pkgbase, group.repo) logger.info("creating spec with SVN message for %s", group.pkgbase) - spec = create_specification(group.packages[0], log) + spec = create_specification(group.packages[0], log, finder) spec.save() # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From f43a33ed8696d7bcb987d4878c6411c5d16846d6 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 17 Nov 2011 13:32:42 -0600 Subject: Display package URLs unquoted if possible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Example: kbd-ru-keymaps. Before: http://wiki.archlinux.org/index.php/%D0%98%D0%BD%D1%82%D0%B5%D1%80%D0%BD%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F After: http://wiki.archlinux.org/index.php/Интернационализация Signed-off-by: Dan McGee --- packages/templatetags/package_extras.py | 13 ++++++++++++- templates/packages/details.html | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) (limited to 'packages') diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py index 01bf7510..67c7fbbc 100644 --- a/packages/templatetags/package_extras.py +++ b/packages/templatetags/package_extras.py @@ -1,4 +1,4 @@ -from urllib import urlencode, quote as urlquote +from urllib import urlencode, quote as urlquote, unquote try: from urlparse import parse_qs except ImportError: @@ -13,6 +13,17 @@ def link_encode(url, query, doseq=False): data = urlencode(query, doseq).replace('&', '&') return "%s?%s" % (url, data) +@register.filter +def url_unquote(original_url): + try: + url = original_url + if isinstance(url, unicode): + url = url.encode('ascii') + url = unquote(url).decode('utf-8') + return url + except UnicodeError: + return original_url + class BuildQueryStringNode(template.Node): def __init__(self, sortfield): self.sortfield = sortfield diff --git a/templates/packages/details.html b/templates/packages/details.html index 2998592f..a9908012 100644 --- a/templates/packages/details.html +++ b/templates/packages/details.html @@ -108,7 +108,7 @@

      Versions Elsewhere

      Upstream URL: {% if pkg.url %}{{ pkg.url }}{% endif %} + title="Visit the website for {{ pkg.pkgname }}">{{ pkg.url|url_unquote }}{% endif %} License(s): {{ pkg.licenses.all|join:", " }} -- cgit v1.2.3-54-g00ecf From 85657db05d7f65604340699cfcb9967c9e81a0ef Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 21 Nov 2011 10:08:23 -0600 Subject: Better support for non-latin full names Add a 'latin_name' field to the user profile so we can better support those developers with names in non-Latin scripts, and yet still show a Latin name as necessary on the developer profile page. This field only shows up if populated. Also, use consistent sorting everywhere- rather than using username, always use first_name and last_name fields. Signed-off-by: Dan McGee --- devel/views.py | 2 +- .../0057_auto__add_field_userprofile_latin_name.py | 153 +++++++++++++++++++++ main/models.py | 2 + packages/views/search.py | 3 +- public/views.py | 6 +- templates/devel/clock.html | 2 +- templates/public/developer_list.html | 2 +- 7 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 main/migrations/0057_auto__add_field_userprofile_latin_name.py (limited to 'packages') diff --git a/devel/views.py b/devel/views.py index 0002df04..08b19cd7 100644 --- a/devel/views.py +++ b/devel/views.py @@ -83,7 +83,7 @@ def index(request): @never_cache def clock(request): devs = User.objects.filter(is_active=True).order_by( - 'username').select_related('userprofile') + 'first_name', 'last_name').select_related('userprofile') now = datetime.now() utc_now = datetime.utcnow().replace(tzinfo=pytz.utc) diff --git a/main/migrations/0057_auto__add_field_userprofile_latin_name.py b/main/migrations/0057_auto__add_field_userprofile_latin_name.py new file mode 100644 index 00000000..ffde1885 --- /dev/null +++ b/main/migrations/0057_auto__add_field_userprofile_latin_name.py @@ -0,0 +1,153 @@ +# encoding: utf-8 +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.add_column('user_profiles', 'latin_name', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True), keep_default=False) + + def backwards(self, orm): + db.delete_column('user_profiles', 'latin_name') + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'main.arch': { + 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"}, + 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'main.donor': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Donor', 'db_table': "'donors'"}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'main.package': { + 'Meta': {'ordering': "('pkgname',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.models.PositiveBigIntegerField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'files_last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'flag_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'installed_size': ('main.models.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Repo']"}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) + }, + 'main.packagedepend': { + 'Meta': {'object_name': 'PackageDepend', 'db_table': "'package_depends'"}, + 'depname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'depvcmp': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}) + }, + 'main.packagefile': { + 'Meta': {'object_name': 'PackageFile', 'db_table': "'package_files'"}, + 'directory': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_directory': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}) + }, + 'main.repo': { + 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"}, + 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'staging': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'svn_root': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'testing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'main.todolist': { + 'Meta': {'object_name': 'Todolist', 'db_table': "'todolists'"}, + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'date_added': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'main.todolistpkg': { + 'Meta': {'unique_together': "(('list', 'pkg'),)", 'object_name': 'TodolistPkg', 'db_table': "'todolist_pkgs'"}, + 'complete': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Todolist']"}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}) + }, + 'main.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'user_profiles'"}, + 'alias': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'allowed_repos': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Repo']", 'symmetrical': 'False', 'blank': 'True'}), + 'favorite_distros': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'interests': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'languages': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'latin_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'notify': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'occupation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'other_contact': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'pgp_key': ('main.models.PGPKeyField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'picture': ('django.db.models.fields.files.FileField', [], {'default': "'devs/silhouette.png'", 'max_length': '100'}), + 'public_email': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'roles': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'time_zone': ('django.db.models.fields.CharField', [], {'default': "'UTC'", 'max_length': '100'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'userprofile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'website': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'yob': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['main'] diff --git a/main/models.py b/main/models.py index b37468f9..d7780b91 100644 --- a/main/models.py +++ b/main/models.py @@ -77,6 +77,8 @@ class UserProfile(models.Model): help_text="Ideally 125px by 125px") user = models.OneToOneField(User, related_name='userprofile') allowed_repos = models.ManyToManyField('Repo', blank=True) + latin_name = models.CharField(max_length=255, null=True, blank=True, + help_text="Latin-form name; used only for non-Latin full names") class Meta: db_table = 'user_profiles' diff --git a/packages/views/search.py b/packages/views/search.py index 57481614..65fcddb3 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -60,7 +60,8 @@ def __init__(self, *args, **kwargs): self.fields['arch'].choices = make_choice( [arch.name for arch in Arch.objects.all()]) self.fields['q'].widget.attrs.update({"size": "30"}) - maints = User.objects.filter(is_active=True).order_by('username') + maints = User.objects.filter(is_active=True).order_by( + 'first_name', 'last_name') self.fields['maintainer'].choices = \ [('', 'All'), ('orphan', 'Orphan')] + \ [(m.username, m.get_full_name()) for m in maints] diff --git a/public/views.py b/public/views.py index 14dd6353..c28fd303 100644 --- a/public/views.py +++ b/public/views.py @@ -34,13 +34,15 @@ def index(request): } def userlist(request, user_type='devs'): - users = User.objects.order_by('username').select_related('userprofile') + users = User.objects.order_by( + 'first_name', 'last_name').select_related('userprofile') if user_type == 'devs': users = users.filter(is_active=True, groups__name="Developers") elif user_type == 'tus': users = users.filter(is_active=True, groups__name="Trusted Users") elif user_type == 'fellows': - users = users.filter(is_active=False, groups__name__in=["Developers", "Trusted Users"]) + users = users.filter(is_active=False, + groups__name__in=["Developers", "Trusted Users"]) else: raise Http404 diff --git a/templates/devel/clock.html b/templates/devel/clock.html index 0f0e20c5..d2eb0a8d 100644 --- a/templates/devel/clock.html +++ b/templates/devel/clock.html @@ -45,7 +45,7 @@

      Developer World Clocks

      {% endblock %} diff --git a/templates/public/developer_list.html b/templates/public/developer_list.html index 0ac444e5..5aa4c6b2 100644 --- a/templates/public/developer_list.html +++ b/templates/public/developer_list.html @@ -21,7 +21,7 @@ - + -- cgit v1.2.3-54-g00ecf
      Name:{{ dev.get_full_name }}{{ dev.get_full_name }}{% if prof.latin_name %} ({{ prof.latin_name}}){% endif %}
      Alias: {{ prof.alias }}