From b59e79f3878d59b83c6867eb5c6196f8f003dcd9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 1 May 2012 17:13:33 -0500 Subject: Opensearch enhancements * Add a 64x64 icon as indicated in the Opensearch specification. * Add suggestions capability and a new view providing suggestions based on package name starting with the typed value. Signed-off-by: Dan McGee --- packages/views/__init__.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'packages/views') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 6a9c5275..21d17470 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -7,7 +7,7 @@ from django.contrib.auth.models import User from django.http import HttpResponse, Http404 from django.shortcuts import get_object_or_404, redirect from django.utils import simplejson -from django.views.decorators.http import require_POST +from django.views.decorators.http import require_GET, require_POST from django.views.decorators.vary import vary_on_headers from django.views.generic.simple import direct_to_template @@ -24,6 +24,7 @@ from .search import search, search_json from .signoff import signoffs, signoff_package, signoff_options, signoffs_json +@require_GET def opensearch(request): if request.is_secure(): domain = "https://%s" % request.META['HTTP_HOST'] @@ -34,6 +35,21 @@ def opensearch(request): {'domain': domain}, mimetype='application/opensearchdescription+xml') + +@require_GET +def opensearch_suggest(request): + search_term = request.GET.get('q', '') + if search_term == '': + return HttpResponse('', mimetype='application/x-suggestions+json') + + names = Package.objects.filter( + pkgname__startswith=search_term).values_list( + 'pkgname', flat=True).order_by('pkgname').distinct()[:10] + results = [search_term, list(names)] + to_json = simplejson.dumps(results, ensure_ascii=False) + return HttpResponse(to_json, mimetype='application/x-suggestions+json') + + @permission_required('main.change_package') @require_POST def update(request): -- cgit v1.2.3-2-g168b From f3e0adcb2fc9a26e2ad9337a47550a37590074d9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 1 May 2012 18:12:50 -0500 Subject: Add some caching to the Opensearch-related views Both some simple cache headers as well as low-level results caching on search terms suggestions. Signed-off-by: Dan McGee --- packages/views/__init__.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) (limited to 'packages/views') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 21d17470..0f1dc799 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -4,9 +4,11 @@ from urllib import urlencode from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import User +from django.core.cache import cache 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 cache_control from django.views.decorators.http import require_GET, require_POST from django.views.decorators.vary import vary_on_headers from django.views.generic.simple import direct_to_template @@ -25,6 +27,7 @@ from .signoff import signoffs, signoff_package, signoff_options, signoffs_json @require_GET +@cache_control(public=True, max_age=86400) def opensearch(request): if request.is_secure(): domain = "https://%s" % request.META['HTTP_HOST'] @@ -37,16 +40,21 @@ def opensearch(request): @require_GET +@cache_control(public=True, max_age=300) def opensearch_suggest(request): search_term = request.GET.get('q', '') if search_term == '': return HttpResponse('', mimetype='application/x-suggestions+json') - names = Package.objects.filter( - pkgname__startswith=search_term).values_list( - 'pkgname', flat=True).order_by('pkgname').distinct()[:10] - results = [search_term, list(names)] - to_json = simplejson.dumps(results, ensure_ascii=False) + cache_key = 'opensearch:packages:' + search_term + to_json = cache.get(cache_key, None) + if to_json is None: + names = Package.objects.filter( + pkgname__startswith=search_term).values_list( + 'pkgname', flat=True).order_by('pkgname').distinct()[:10] + results = [search_term, list(names)] + to_json = simplejson.dumps(results, ensure_ascii=False) + cache.set('opensearch:packages:%s' % search_term, to_json, 300) return HttpResponse(to_json, mimetype='application/x-suggestions+json') -- cgit v1.2.3-2-g168b From 768bc688aab844cf9fdf9809b9381aaf0042f2fc Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 2 May 2012 10:21:30 -0500 Subject: Flagging related cleanups and improvements Touch up the style slightly on the flag help popup to match the main site style more closely. When a logged-in user is flagging a package out of date, we have no need for them to fill in the email field since we already have an email address on file. Signed-off-by: Dan McGee --- packages/views/flag.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) (limited to 'packages/views') diff --git a/packages/views/flag.py b/packages/views/flag.py index 0d2f9009..7fa2d508 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -6,16 +6,13 @@ from django.db import transaction 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 django.views.decorators.cache import cache_page, never_cache from ..models import FlagRequest from main.models import Package from main.utils import utc_now -def flaghelp(request): - return direct_to_template(request, 'packages/flaghelp.html') - class FlagForm(forms.Form): email = forms.EmailField(label='E-mail Address') message = forms.CharField(label='Message To Developer', @@ -26,6 +23,20 @@ class FlagForm(forms.Form): widget=forms.TextInput(attrs={'style': 'display:none;'}), required=False) + def __init__(self, *args, **kwargs): + # we remove the 'email' field if this form is being shown to a + # logged-in user, e.g., a developer. + auth = kwargs.pop('authenticated', False) + super(FlagForm, self).__init__(*args, **kwargs) + if auth: + del self.fields['email'] + + +@cache_page(3600) +def flaghelp(request): + return direct_to_template(request, 'packages/flaghelp.html') + + @never_cache def flag(request, name, repo, arch): pkg = get_object_or_404(Package, @@ -41,8 +52,10 @@ def flag(request, name, repo, arch): repo__staging=pkg.repo.staging).order_by( 'pkgname', 'repo__name', 'arch__name') + authenticated = request.user.is_authenticated() + if request.POST: - form = FlagForm(request.POST) + form = FlagForm(request.POST, authenticated=authenticated) if form.is_valid() and form.cleaned_data['website'] == '': # save the package list for later use flagged_pkgs = list(pkgs) @@ -54,9 +67,12 @@ def flag(request, name, repo, arch): else: version = '' - email = form.cleaned_data['email'] message = form.cleaned_data['message'] ip_addr = request.META.get('REMOTE_ADDR') + if authenticated: + email = request.user.email + else: + email = form.cleaned_data['email'] @transaction.commit_on_success def perform_updates(): @@ -68,7 +84,7 @@ def flag(request, name, repo, arch): ip_address=ip_addr, pkgbase=pkg.pkgbase, version=version, repo=pkg.repo, num_packages=len(flagged_pkgs)) - if request.user.is_authenticated(): + if authenticated: flag_request.user = request.user flag_request.save() @@ -106,9 +122,7 @@ def flag(request, name, repo, arch): arch=arch) else: initial = {} - if request.user.is_authenticated(): - initial['email'] = request.user.email - form = FlagForm(initial=initial) + form = FlagForm(authenticated=authenticated) context = { 'package': pkg, -- cgit v1.2.3-2-g168b From 86f8efaeb1f67138c194d0c373f9d91e2999c5dd Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 4 May 2012 10:57:59 -0500 Subject: Fix search suggestions for invalid cache keys Unfortunately, "invalid" in this case includes spaces, which is a bit crazy. MD5 the provided search term before using it as a cache key to be safe. Signed-off-by: Dan McGee --- packages/views/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'packages/views') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 0f1dc799..559368b9 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -1,3 +1,4 @@ +import hashlib from string import Template from urllib import urlencode @@ -46,7 +47,7 @@ def opensearch_suggest(request): if search_term == '': return HttpResponse('', mimetype='application/x-suggestions+json') - cache_key = 'opensearch:packages:' + search_term + cache_key = 'opensearch:packages:' + hashlib.md5(search_term).hexdigest() to_json = cache.get(cache_key, None) if to_json is None: names = Package.objects.filter( @@ -54,7 +55,7 @@ def opensearch_suggest(request): 'pkgname', flat=True).order_by('pkgname').distinct()[:10] results = [search_term, list(names)] to_json = simplejson.dumps(results, ensure_ascii=False) - cache.set('opensearch:packages:%s' % search_term, to_json, 300) + cache.set(cache_key, to_json, 300) return HttpResponse(to_json, mimetype='application/x-suggestions+json') -- cgit v1.2.3-2-g168b From d4c7a48623f90cdc508d1824bf47ce3e398dd820 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 5 May 2012 10:19:49 -0500 Subject: Fix suggestion caching again for non-ASCII characters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is why you should test this stuff with random input before rolling it out. Whoops. URL that caught this problem: /opensearch/packages/suggest?q=%D7%A0%D7%9F%D7%92%D7%9F aka /opensearch/packages/suggest?q=נןגן Signed-off-by: Dan McGee --- packages/views/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'packages/views') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 559368b9..60c3a46b 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -47,7 +47,8 @@ def opensearch_suggest(request): if search_term == '': return HttpResponse('', mimetype='application/x-suggestions+json') - cache_key = 'opensearch:packages:' + hashlib.md5(search_term).hexdigest() + cache_key = 'opensearch:packages:' + \ + hashlib.md5(search_term.encode('utf-8')).hexdigest() to_json = cache.get(cache_key, None) if to_json is None: names = Package.objects.filter( -- cgit v1.2.3-2-g168b From a5f5557493446bede78adb0584c88208234f874e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 12 May 2012 09:32:30 -0500 Subject: Use python json module directly in place of simplejson As of Python 2.6, this is a builtin module that has all the same functions and capabilities of the Django simplejson module. Additionally simplejson is deprecated in the upcoming Django 1.5 release. Signed-off-by: Dan McGee --- packages/views/__init__.py | 10 ++++------ packages/views/search.py | 5 ++--- packages/views/signoff.py | 7 +++---- 3 files changed, 9 insertions(+), 13 deletions(-) (limited to 'packages/views') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 60c3a46b..c1a035d0 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -1,4 +1,5 @@ import hashlib +import json from string import Template from urllib import urlencode @@ -8,7 +9,6 @@ from django.contrib.auth.models import User from django.core.cache import cache 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 cache_control from django.views.decorators.http import require_GET, require_POST from django.views.decorators.vary import vary_on_headers @@ -55,7 +55,7 @@ def opensearch_suggest(request): pkgname__startswith=search_term).values_list( 'pkgname', flat=True).order_by('pkgname').distinct()[:10] results = [search_term, list(names)] - to_json = simplejson.dumps(results, ensure_ascii=False) + to_json = json.dumps(results, ensure_ascii=False) cache.set(cache_key, to_json, 300) return HttpResponse(to_json, mimetype='application/x-suggestions+json') @@ -197,8 +197,7 @@ def files(request, name, repo, arch): 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) + to_json = json.dumps(pkg, ensure_ascii=False, cls=PackageJSONEncoder) return HttpResponse(to_json, mimetype='application/json') def files_json(request, name, repo, arch): @@ -212,8 +211,7 @@ def files_json(request, name, repo, arch): 'arch': pkg.arch.name.lower(), 'files': fileslist, } - to_json = simplejson.dumps(data, ensure_ascii=False, - cls=PackageJSONEncoder) + to_json = json.dumps(data, ensure_ascii=False, cls=PackageJSONEncoder) return HttpResponse(to_json, mimetype='application/json') def download(request, name, repo, arch): diff --git a/packages/views/search.py b/packages/views/search.py index a09de0a7..a89822be 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -1,4 +1,5 @@ from datetime import datetime +import json from django import forms from django.contrib.admin.widgets import AdminDateWidget @@ -6,7 +7,6 @@ from django.contrib.auth.models import User from django.db.models import Q from django.http import HttpResponse from django.views.generic import list_detail -from django.utils import simplejson from main.models import Package, Arch, Repo from main.utils import make_choice @@ -179,8 +179,7 @@ def search_json(request): container['results'] = packages container['valid'] = True - to_json = simplejson.dumps(container, ensure_ascii=False, - cls=PackageJSONEncoder) + to_json = json.dumps(container, ensure_ascii=False, cls=PackageJSONEncoder) return HttpResponse(to_json, mimetype='application/json') # vim: set ts=4 sw=4 et: diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 63341a1d..61d949fc 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -1,3 +1,4 @@ +import json from operator import attrgetter from django import forms @@ -7,7 +8,6 @@ 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 @@ -67,7 +67,7 @@ def signoff_package(request, name, repo, arch, revoke=False): 'known_bad': spec.known_bad, 'user': str(request.user), } - return HttpResponse(simplejson.dumps(data, ensure_ascii=False), + return HttpResponse(json.dumps(data, ensure_ascii=False), mimetype='application/json') return redirect('package-signoffs') @@ -183,8 +183,7 @@ def signoffs_json(request): 'version': 2, 'signoff_groups': signoff_groups, } - to_json = simplejson.dumps(data, ensure_ascii=False, - cls=SignoffJSONEncoder) + to_json = json.dumps(data, ensure_ascii=False, cls=SignoffJSONEncoder) response = HttpResponse(to_json, mimetype='application/json') return response -- cgit v1.2.3-2-g168b From cf67e7952396121d3f7190195d812ea3f5fc7dcf Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 May 2012 20:26:16 -0500 Subject: Issue redirects from non-agnostic to agnostic URLs if unambiguous For something like "/extra/i686/apache-ant/", we can redirect to "/extra/any/apache-ant/" without ambiguity. Previously this redirected to the split packages listing with a single package, which was neither correct nor really expected. Signed-off-by: Dan McGee --- packages/views/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'packages/views') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index c1a035d0..3e574c26 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -136,6 +136,16 @@ def details(request, name='', repo='', arch=''): return direct_to_template(request, 'packages/details.html', {'pkg': pkg, }) except Package.DoesNotExist: + arch_obj = get_object_or_404(Arch, name=arch) + # for arch='any' packages, we can issue a redirect to them if we + # have a single non-ambiguous option by changing the arch to match + # any arch-agnostic package + if not arch_obj.agnostic: + pkgs = Package.objects.select_related( + 'arch', 'repo', 'packager').filter(pkgname=name, + repo__name__iexact=repo, arch__agnostic=True) + if len(pkgs) == 1: + return redirect(pkgs[0], permanent=True) return split_package_details(request, name, repo, arch) else: pkg_data = [ -- cgit v1.2.3-2-g168b From 872d4bcaa2ce85d2d319a1146e0fc05ab6808eb9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 21 Jul 2012 11:26:34 -0500 Subject: Split details/display package views into new module This moves a lot of the package and group display logic into a new view module, similar to what we already did earlier with a bunch of other views. Signed-off-by: Dan McGee --- packages/views/__init__.py | 154 ++----------------------------------------- packages/views/display.py | 160 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 147 deletions(-) create mode 100644 packages/views/display.py (limited to 'packages/views') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 3e574c26..fa67daa8 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -1,27 +1,24 @@ import hashlib import json -from string import Template -from urllib import urlencode from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import User from django.core.cache import cache -from django.http import HttpResponse, Http404 -from django.shortcuts import get_object_or_404, redirect +from django.http import HttpResponse +from django.shortcuts import redirect from django.views.decorators.cache import cache_control from django.views.decorators.http import require_GET, require_POST -from django.views.decorators.vary import vary_on_headers from django.views.generic.simple import direct_to_template -from main.models import Package, PackageFile, Arch, Repo -from mirrors.models import MirrorUrl -from mirrors.utils import get_mirror_url_for_download +from main.models import Package, Arch from ..models import PackageRelation -from ..utils import (get_group_info, get_differences_info, - multilib_differences, get_wrong_permissions, PackageJSONEncoder) +from ..utils import (get_differences_info, + multilib_differences, get_wrong_permissions) # make other views available from this same package +from .display import (details, groups, group_details, files, details_json, + files_json, download) from .flag import flaghelp, flag, flag_confirmed, unflag, unflag_all from .search import search, search_json from .signoff import signoffs, signoff_package, signoff_options, signoffs_json @@ -105,143 +102,6 @@ def update(request): messages.error(request, "Are you trying to adopt or disown?") return redirect('/packages/') -def split_package_details(request, name='', repo='', arch=''): - 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 - # we have packages, but ensure at least one is in the given repo - if not any(True for pkg in pkgs if pkg.repo == repo): - raise Http404 - context = { - 'list_title': 'Split Package Details', - 'name': name, - 'arch': arch, - 'packages': pkgs, - } - return direct_to_template(request, 'packages/packages_list.html', - context) - -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_obj = get_object_or_404(Arch, name=arch) - # for arch='any' packages, we can issue a redirect to them if we - # have a single non-ambiguous option by changing the arch to match - # any arch-agnostic package - if not arch_obj.agnostic: - pkgs = Package.objects.select_related( - 'arch', 'repo', 'packager').filter(pkgname=name, - repo__name__iexact=repo, arch__agnostic=True) - if len(pkgs) == 1: - return redirect(pkgs[0], permanent=True) - return split_package_details(request, name, repo, arch) - 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.encode('utf-8')) 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) - -@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) - # files are inserted in sorted order, so preserve that - fileslist = PackageFile.objects.filter(pkg=pkg).order_by('id') - dir_count = sum(1 for f in fileslist if f.is_directory) - files_count = len(fileslist) - dir_count - context = { - 'pkg': pkg, - 'files': fileslist, - 'files_count': files_count, - 'dir_count': dir_count, - } - 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 = json.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) - # files are inserted in sorted order, so preserve that - fileslist = PackageFile.objects.filter(pkg=pkg).order_by('id') - data = { - 'pkgname': pkg.pkgname, - 'repo': pkg.repo.name.lower(), - 'arch': pkg.arch.name.lower(), - 'files': fileslist, - } - to_json = json.dumps(data, ensure_ascii=False, cls=PackageJSONEncoder) - return HttpResponse(to_json, mimetype='application/json') - -def download(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) - url = get_mirror_url_for_download() - if not url: - 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': url.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. diff --git a/packages/views/display.py b/packages/views/display.py new file mode 100644 index 00000000..dd57571a --- /dev/null +++ b/packages/views/display.py @@ -0,0 +1,160 @@ +import json +from string import Template +from urllib import urlencode + +from django.http import HttpResponse, Http404 +from django.shortcuts import get_object_or_404, redirect +from django.views.generic.simple import direct_to_template +from django.views.decorators.vary import vary_on_headers + +from main.models import Package, PackageFile, Arch, Repo +from mirrors.utils import get_mirror_url_for_download +from ..utils import get_group_info, PackageJSONEncoder + + +def split_package_details(request, name='', repo='', arch=''): + 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 + # we have packages, but ensure at least one is in the given repo + if not any(True for pkg in pkgs if pkg.repo == repo): + raise Http404 + context = { + 'list_title': 'Split Package Details', + 'name': name, + 'arch': arch, + 'packages': pkgs, + } + return direct_to_template(request, 'packages/packages_list.html', + context) + + +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_obj = get_object_or_404(Arch, name=arch) + # for arch='any' packages, we can issue a redirect to them if we + # have a single non-ambiguous option by changing the arch to match + # any arch-agnostic package + if not arch_obj.agnostic: + pkgs = Package.objects.select_related( + 'arch', 'repo', 'packager').filter(pkgname=name, + repo__name__iexact=repo, arch__agnostic=True) + if len(pkgs) == 1: + return redirect(pkgs[0], permanent=True) + return split_package_details(request, name, repo, arch) + 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.encode('utf-8')) 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) + + +@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) + # files are inserted in sorted order, so preserve that + fileslist = PackageFile.objects.filter(pkg=pkg).order_by('id') + dir_count = sum(1 for f in fileslist if f.is_directory) + files_count = len(fileslist) - dir_count + context = { + 'pkg': pkg, + 'files': fileslist, + 'files_count': files_count, + 'dir_count': dir_count, + } + 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 = json.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) + # files are inserted in sorted order, so preserve that + fileslist = PackageFile.objects.filter(pkg=pkg).order_by('id') + data = { + 'pkgname': pkg.pkgname, + 'repo': pkg.repo.name.lower(), + 'arch': pkg.arch.name.lower(), + 'files': fileslist, + } + to_json = json.dumps(data, ensure_ascii=False, cls=PackageJSONEncoder) + return HttpResponse(to_json, mimetype='application/json') + + +def download(request, name, repo, arch): + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) + url = get_mirror_url_for_download() + if not url: + 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': url.url, + 'arch': arch, + 'repo': pkg.repo.name.lower(), + 'file': pkg.filename, + } + url = Template('${host}${repo}/os/${arch}/${file}').substitute(values) + return redirect(url) + +# vim: set ts=4 sw=4 et: -- cgit v1.2.3-2-g168b From 946d90d08f29094153142056a1778cd595e568a3 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 23 Jul 2012 09:45:44 -0500 Subject: Add '410 Gone' support for packages moved out of repositories This allows us to do better than a generic 404 handler when we know a package previously existed in a given repository, and should also make things a bit nicer when getting sent in from a search engine to a page that no longer exists. Signed-off-by: Dan McGee --- packages/views/display.py | 46 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) (limited to 'packages/views') diff --git a/packages/views/display.py b/packages/views/display.py index dd57571a..5332dc13 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -1,18 +1,21 @@ +import datetime import json from string import Template from urllib import urlencode from django.http import HttpResponse, Http404 -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import get_object_or_404, redirect, render +from django.utils.timezone import now from django.views.generic.simple import direct_to_template from django.views.decorators.vary import vary_on_headers from main.models import Package, PackageFile, Arch, Repo from mirrors.utils import get_mirror_url_for_download +from ..models import Update from ..utils import get_group_info, PackageJSONEncoder -def split_package_details(request, name='', repo='', arch=''): +def split_package_details(request, name, repo, arch): arch = get_object_or_404(Arch, name=arch) arches = [ arch ] arches.extend(Arch.objects.filter(agnostic=True)) @@ -21,10 +24,10 @@ def split_package_details(request, name='', repo='', arch=''): repo__testing=repo.testing, repo__staging=repo.staging, arch__in=arches).order_by('pkgname') if len(pkgs) == 0: - raise Http404 + return None # we have packages, but ensure at least one is in the given repo if not any(True for pkg in pkgs if pkg.repo == repo): - raise Http404 + return None context = { 'list_title': 'Split Package Details', 'name': name, @@ -35,6 +38,30 @@ def split_package_details(request, name='', repo='', arch=''): context) +CUTOFF = datetime.timedelta(days=60) + + +def recently_removed_package(request, name, repo, arch, cutoff=CUTOFF): + '''We're just steps away from raising a 404, but check our packages update + table first to see if this package has existed in this repo before. If so, + we can show a 410 Gone page and point the requester in the right + direction.''' + arch = get_object_or_404(Arch, name=arch) + arches = [ arch ] + arches.extend(Arch.objects.filter(agnostic=True)) + match = Update.objects.select_related('arch', 'repo').filter( + pkgname=name, repo__name__iexact=repo, arch__in=arches) + if cutoff is not None: + when = now() - cutoff + match = match.filter(created__gte=when) + try: + match = match.latest() + return render(request, 'packages/removed.html', + {'update': match, }, status=410) + except Update.DoesNotExist: + return None + + def details(request, name='', repo='', arch=''): if all([name, repo, arch]): try: @@ -54,7 +81,16 @@ def details(request, name='', repo='', arch=''): repo__name__iexact=repo, arch__agnostic=True) if len(pkgs) == 1: return redirect(pkgs[0], permanent=True) - return split_package_details(request, name, repo, arch) + # do we have a split package matching this criteria? + ret = split_package_details(request, name, repo, arch) + if ret is None: + # maybe we have a recently-removed package? + ret = recently_removed_package(request, name, repo, arch) + if ret is not None: + return ret + else: + # we've tried everything at this point, nothing to see + raise Http404 else: pkg_data = [ ('arch', arch.lower()), -- cgit v1.2.3-2-g168b From 374bf53505480eebd675e95f975dc908c485a6fa Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 23 Jul 2012 21:17:02 -0500 Subject: Remove files list AJAX conditionals Now that we just generate this list in JS, we don't need this separate code. Signed-off-by: Dan McGee --- packages/views/display.py | 4 ---- 1 file changed, 4 deletions(-) (limited to 'packages/views') diff --git a/packages/views/display.py b/packages/views/display.py index 5332dc13..d5aa2c20 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -7,7 +7,6 @@ from django.http import HttpResponse, Http404 from django.shortcuts import get_object_or_404, redirect, render from django.utils.timezone import now from django.views.generic.simple import direct_to_template -from django.views.decorators.vary import vary_on_headers from main.models import Package, PackageFile, Arch, Repo from mirrors.utils import get_mirror_url_for_download @@ -132,7 +131,6 @@ def group_details(request, arch, name): return direct_to_template(request, 'packages/packages_list.html', context) -@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) @@ -147,8 +145,6 @@ def files(request, name, repo, arch): 'dir_count': dir_count, } template = 'packages/files.html' - if request.is_ajax(): - template = 'packages/files_list.html' return direct_to_template(request, template, context) -- cgit v1.2.3-2-g168b From 211340c8bd6ccd6b16f3115a71fce4abedcc4c06 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 23 Jul 2012 21:31:17 -0500 Subject: Ensure package files JS can support corner cases We should handle the cases dealing with no filelist available, outdated filelist, or a package without files, just as the HTML server-side page does. Add a bit more info to the JSON returned so we can do so. Signed-off-by: Dan McGee --- packages/views/display.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'packages/views') diff --git a/packages/views/display.py b/packages/views/display.py index d5aa2c20..02f5a5b2 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -160,10 +160,16 @@ def files_json(request, name, repo, arch): pkgname=name, repo__name__iexact=repo, arch__name=arch) # files are inserted in sorted order, so preserve that fileslist = PackageFile.objects.filter(pkg=pkg).order_by('id') + dir_count = sum(1 for f in fileslist if f.is_directory) + files_count = len(fileslist) - dir_count data = { 'pkgname': pkg.pkgname, 'repo': pkg.repo.name.lower(), 'arch': pkg.arch.name.lower(), + 'pkg_last_update': pkg.last_update, + 'files_last_update': pkg.files_last_update, + 'files_count': files_count, + 'dir_count': dir_count, 'files': fileslist, } to_json = json.dumps(data, ensure_ascii=False, cls=PackageJSONEncoder) -- cgit v1.2.3-2-g168b From 9ab460c53a1ac4c79da6f05f2850ee21beedbab2 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 23 Jul 2012 21:47:43 -0500 Subject: Fall back to 410 Gone for package files view as well This is another thing that Google and other search engines try to crawl that no longer exists at times, so we should handle it gracefully. Signed-off-by: Dan McGee --- packages/views/display.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'packages/views') diff --git a/packages/views/display.py b/packages/views/display.py index 02f5a5b2..31f18c79 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -132,8 +132,16 @@ def group_details(request, arch, name): def files(request, name, repo, arch): - pkg = get_object_or_404(Package, - pkgname=name, repo__name__iexact=repo, arch__name=arch) + try: + pkg = Package.objects.get(pkgname=name, + repo__name__iexact=repo, arch__name=arch) + except Package.DoesNotExist: + # this may have been deleted recently, so follow the same logic as we + # do on the package details page if possible + ret = recently_removed_package(request, name, repo, arch) + if ret is not None: + return ret + raise Http404 # files are inserted in sorted order, so preserve that fileslist = PackageFile.objects.filter(pkg=pkg).order_by('id') dir_count = sum(1 for f in fileslist if f.is_directory) -- cgit v1.2.3-2-g168b From c0bf9e20660cfae7ea8994472555bba23398b598 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 24 Jul 2012 09:19:48 -0500 Subject: Remove custom utc_now() function, use django.utils.timezone.now() This was around from the time when we handled timezones sanely and Django did not; now that we are on 1.4 we no longer need our own code to handle this. Signed-off-by: Dan McGee --- packages/views/flag.py | 8 ++++---- packages/views/signoff.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'packages/views') diff --git a/packages/views/flag.py b/packages/views/flag.py index 7fa2d508..f3db93b3 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -5,12 +5,12 @@ from django.core.mail import send_mail from django.db import transaction from django.shortcuts import get_object_or_404, redirect from django.template import loader, Context +from django.utils.timezone import now from django.views.generic.simple import direct_to_template from django.views.decorators.cache import cache_page, never_cache from ..models import FlagRequest from main.models import Package -from main.utils import utc_now class FlagForm(forms.Form): @@ -76,10 +76,10 @@ def flag(request, name, repo, arch): @transaction.commit_on_success def perform_updates(): - now = utc_now() - pkgs.update(flag_date=now) + current_time = now() + pkgs.update(flag_date=current_time) # store our flag request - flag_request = FlagRequest(created=now, + flag_request = FlagRequest(created=current_time, user_email=email, message=message, ip_address=ip_addr, pkgbase=pkg.pkgbase, version=version, repo=pkg.repo, diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 61d949fc..7aa39106 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -8,11 +8,11 @@ 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.timezone import now 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 main.utils import utc_now from ..models import SignoffSpecification, Signoff from ..utils import (get_signoff_groups, approved_by_signoffs, PackageSignoffGroup) @@ -45,7 +45,7 @@ def signoff_package(request, name, repo, arch, revoke=False): package, request.user, False) except Signoff.DoesNotExist: raise Http404 - signoff.revoked = utc_now() + signoff.revoked = now() signoff.save() created = False else: -- cgit v1.2.3-2-g168b From 76c37ce3acc7a4af0271c7535d4a33042f7749b5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 24 Jul 2012 09:35:55 -0500 Subject: Replace deprecated direct_to_template() with render() shortcut Now that Django actually provides a concise way to use a RequestContext object without instantiating it, we can use that rather than the old function-based generic view that worked well to do the same. Additionally, these function-based generic views will be gone in Django 1.5, so might as well make the move now. Signed-off-by: Dan McGee --- packages/views/__init__.py | 11 +++++------ packages/views/display.py | 13 +++++-------- packages/views/flag.py | 12 +++++------- packages/views/signoff.py | 5 ++--- 4 files changed, 17 insertions(+), 24 deletions(-) (limited to 'packages/views') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index fa67daa8..19ea9103 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -6,10 +6,9 @@ from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import User from django.core.cache import cache from django.http import HttpResponse -from django.shortcuts import redirect +from django.shortcuts import redirect, render from django.views.decorators.cache import cache_control from django.views.decorators.http import require_GET, require_POST -from django.views.generic.simple import direct_to_template from main.models import Package, Arch from ..models import PackageRelation @@ -32,9 +31,9 @@ def opensearch(request): else: domain = "http://%s" % request.META['HTTP_HOST'] - return direct_to_template(request, 'packages/opensearch.xml', + return render(request, 'packages/opensearch.xml', {'domain': domain}, - mimetype='application/opensearchdescription+xml') + content_type='application/opensearchdescription+xml') @require_GET @@ -115,7 +114,7 @@ def arch_differences(request): 'differences': differences, 'multilib_differences': multilib_diffs } - return direct_to_template(request, 'packages/differences.html', context) + return render(request, 'packages/differences.html', context) @permission_required('main.change_package') def stale_relations(request): @@ -132,7 +131,7 @@ def stale_relations(request): 'missing_pkgbase': missing_pkgbase, 'wrong_permissions': wrong_permissions, } - return direct_to_template(request, 'packages/stale_relations.html', context) + return render(request, 'packages/stale_relations.html', context) @permission_required('packages.delete_packagerelation') @require_POST diff --git a/packages/views/display.py b/packages/views/display.py index 31f18c79..585e0e4e 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -6,7 +6,6 @@ from urllib import urlencode from django.http import HttpResponse, Http404 from django.shortcuts import get_object_or_404, redirect, render from django.utils.timezone import now -from django.views.generic.simple import direct_to_template from main.models import Package, PackageFile, Arch, Repo from mirrors.utils import get_mirror_url_for_download @@ -33,8 +32,7 @@ def split_package_details(request, name, repo, arch): 'arch': arch, 'packages': pkgs, } - return direct_to_template(request, 'packages/packages_list.html', - context) + return render(request, 'packages/packages_list.html', context) CUTOFF = datetime.timedelta(days=60) @@ -67,8 +65,7 @@ def details(request, name='', repo='', arch=''): 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, }) + return render(request, 'packages/details.html', {'pkg': pkg}) except Package.DoesNotExist: arch_obj = get_object_or_404(Arch, name=arch) # for arch='any' packages, we can issue a redirect to them if we @@ -111,7 +108,7 @@ def groups(request, arch=None): 'groups': grps, 'arch': arch, } - return direct_to_template(request, 'packages/groups.html', context) + return render(request, 'packages/groups.html', context) def group_details(request, arch, name): @@ -128,7 +125,7 @@ def group_details(request, arch, name): 'arch': arch, 'packages': pkgs, } - return direct_to_template(request, 'packages/packages_list.html', context) + return render(request, 'packages/packages_list.html', context) def files(request, name, repo, arch): @@ -153,7 +150,7 @@ def files(request, name, repo, arch): 'dir_count': dir_count, } template = 'packages/files.html' - return direct_to_template(request, template, context) + return render(request, template, context) def details_json(request, name, repo, arch): diff --git a/packages/views/flag.py b/packages/views/flag.py index f3db93b3..b9542a62 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -3,10 +3,9 @@ from django.conf import settings from django.contrib.auth.decorators import permission_required from django.core.mail import send_mail from django.db import transaction -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import get_object_or_404, redirect, render from django.template import loader, Context from django.utils.timezone import now -from django.views.generic.simple import direct_to_template from django.views.decorators.cache import cache_page, never_cache from ..models import FlagRequest @@ -34,7 +33,7 @@ class FlagForm(forms.Form): @cache_page(3600) def flaghelp(request): - return direct_to_template(request, 'packages/flaghelp.html') + return render(request, 'packages/flaghelp.html') @never_cache @@ -43,8 +42,7 @@ def flag(request, name, repo, arch): 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}) + return render(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, @@ -129,7 +127,7 @@ def flag(request, name, repo, arch): 'packages': pkgs, 'form': form } - return direct_to_template(request, 'packages/flag.html', context) + return render(request, 'packages/flag.html', context) def flag_confirmed(request, name, repo, arch): pkg = get_object_or_404(Package, @@ -142,7 +140,7 @@ def flag_confirmed(request, name, repo, arch): context = {'package': pkg, 'packages': pkgs} - return direct_to_template(request, 'packages/flag_confirmed.html', context) + return render(request, 'packages/flag_confirmed.html', context) @permission_required('main.change_package') def unflag(request, name, repo, arch): diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 7aa39106..56eb060c 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -10,7 +10,6 @@ from django.http import HttpResponse, Http404 from django.shortcuts import get_list_or_404, redirect, render from django.utils.timezone import now 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 @@ -28,7 +27,7 @@ def signoffs(request): '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) + return render(request, 'packages/signoffs.html', context) @permission_required('main.change_package') @never_cache @@ -144,7 +143,7 @@ def signoff_options(request, name, repo, arch): 'package': package, 'form': form, } - return direct_to_template(request, 'packages/signoff_options.html', context) + return render(request, 'packages/signoff_options.html', context) class SignoffJSONEncoder(DjangoJSONEncoder): '''Base JSONEncoder extended to handle all serialization of all classes -- cgit v1.2.3-2-g168b From 24b28a504cabcf077882aa95cfa0edbc6a8d4569 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 24 Jul 2012 21:01:31 -0500 Subject: Replace deprecated list_detail usage in search with class-based view We can convert the entire search view to a generic class-based ListView. This is still one of the more disgusting views in the application and has a ton of logic scattered buckshot across several methods, but this commit is not meant to address all of that in one go. This is the last of the deprecated pieces I know of we are still using in the codebase, so we should be relatively safe in the long run now for an upgrade to the eventual next major Django release. Signed-off-by: Dan McGee --- packages/views/__init__.py | 2 +- packages/views/search.py | 77 +++++++++++++++++++++++----------------------- 2 files changed, 40 insertions(+), 39 deletions(-) (limited to 'packages/views') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 19ea9103..038d40ac 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -19,7 +19,7 @@ from ..utils import (get_differences_info, from .display import (details, groups, group_details, files, details_json, files_json, download) from .flag import flaghelp, flag, flag_confirmed, unflag, unflag_all -from .search import search, search_json +from .search import search_json from .signoff import signoffs, signoff_package, signoff_options, signoffs_json diff --git a/packages/views/search.py b/packages/views/search.py index a89822be..9750894a 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -6,7 +6,7 @@ from django.contrib.admin.widgets import AdminDateWidget from django.contrib.auth.models import User from django.db.models import Q from django.http import HttpResponse -from django.views.generic import list_detail +from django.views.generic import ListView from main.models import Package, Arch, Repo from main.utils import make_choice @@ -33,6 +33,7 @@ class LimitTypedChoiceField(forms.TypedChoiceField): except (ValueError, TypeError): return False + class PackageSearchForm(forms.Form): repo = forms.MultipleChoiceField(required=False) arch = forms.MultipleChoiceField(required=False) @@ -69,6 +70,7 @@ class PackageSearchForm(forms.Form): [('', '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( @@ -117,48 +119,47 @@ def parse_form(form, packages): 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(): - packages = parse_form(form, packages) - asked_limit = form.cleaned_data['limit'] +class SearchListView(ListView): + template_name = "packages/search.html" + + sort_fields = ("arch", "repo", "pkgname", "pkgbase", "compressed_size", + "installed_size", "build_date", "last_update", "flag_date") + allowed_sort = list(sort_fields) + ["-" + s for s in sort_fields] + + def get(self, request, *args, **kwargs): + self.form = PackageSearchForm(data=request.GET) + return super(SearchListView, self).get(request, *args, **kwargs) + + def get_queryset(self): + packages = Package.objects.normal() + if self.form.is_valid(): + packages = parse_form(self.form, packages) + sort = self.form.cleaned_data['sort'] + if sort in self.allowed_sort: + packages = packages.order_by(sort) + else: + packages = packages.order_by('pkgname') + return packages + + # Form had errors so don't return any results + return Package.objects.none() + + def get_paginate_by(self, queryset): + limit = 50 + if self.form.is_valid(): + asked_limit = self.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() - 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] - 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) + return limit + + def get_context_data(self, **kwargs): + context = super(SearchListView, self).get_context_data(**kwargs) + context['current_query'] = self.request.GET.urlencode() + context['search_form'] = self.form + return context def search_json(request): -- cgit v1.2.3-2-g168b From 4ad43fd8165834b26914ff8ba0666ce96267205b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 00:25:19 -0500 Subject: Fix broken hidden input sort field on search form Signed-off-by: Dan McGee --- packages/views/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/views') diff --git a/packages/views/search.py b/packages/views/search.py index 9750894a..497d9bca 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -40,7 +40,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) + sort = forms.CharField(required=False, widget=forms.HiddenInput()) maintainer = forms.ChoiceField(required=False) packager = forms.ChoiceField(required=False) last_update = forms.DateField(required=False, widget=AdminDateWidget(), -- cgit v1.2.3-2-g168b From 0b7939ae1a1e3ce55ee458d24fd5946542d9c14a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 19:01:45 -0500 Subject: Rework package details dispatch code We had a variety of fallback paths that we took if a details page didn't exist for the combination of URL parts passed in. Before I go adding a few more possibilities, rework this so it is more flexible. It is now as simple as adding a method to the dispatch options list in order to have further fallback options. Signed-off-by: Dan McGee --- packages/views/display.py | 82 ++++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 37 deletions(-) (limited to 'packages/views') diff --git a/packages/views/display.py b/packages/views/display.py index 585e0e4e..1ea9ea48 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -14,10 +14,10 @@ from ..utils import get_group_info, PackageJSONEncoder def split_package_details(request, name, repo, arch): - arch = get_object_or_404(Arch, name=arch) + '''Check if we have a split package (e.g. pkgbase) value matching this + name. If so, we can show a listing page for the entire set of packages.''' 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') @@ -39,15 +39,13 @@ CUTOFF = datetime.timedelta(days=60) def recently_removed_package(request, name, repo, arch, cutoff=CUTOFF): - '''We're just steps away from raising a 404, but check our packages update - table first to see if this package has existed in this repo before. If so, - we can show a 410 Gone page and point the requester in the right - direction.''' - arch = get_object_or_404(Arch, name=arch) + '''Check our packages update table to see if this package has existed in + this repo before. If so, we can show a 410 Gone page and point the + requester in the right direction.''' arches = [ arch ] arches.extend(Arch.objects.filter(agnostic=True)) match = Update.objects.select_related('arch', 'repo').filter( - pkgname=name, repo__name__iexact=repo, arch__in=arches) + pkgname=name, repo=repo, arch__in=arches) if cutoff is not None: when = now() - cutoff match = match.filter(created__gte=when) @@ -59,43 +57,53 @@ def recently_removed_package(request, name, repo, arch, cutoff=CUTOFF): return None +def redirect_agnostic(request, name, repo, arch): + '''For arch='any' packages, we can issue a redirect to them if we have a + single non-ambiguous option by changing the arch to match any arch-agnostic + package.''' + if not arch.agnostic: + # limit to 2 results, we only need to know whether there is anything + # except only one matching result + pkgs = Package.objects.select_related( + 'arch', 'repo', 'packager').filter(pkgname=name, + repo=repo, arch__agnostic=True)[:2] + if len(pkgs) == 1: + return redirect(pkgs[0], permanent=True) + return None + + +def redirect_to_search(request, name, repo, arch): + pkg_data = [ + ('arch', arch.lower()), + ('repo', repo.lower()), + ('q', name), + ] + # only include non-blank values in the query we generate + pkg_data = [(x, y.encode('utf-8')) for x, y in pkg_data if y] + return redirect("/packages/?%s" % urlencode(pkg_data)) + + def details(request, name='', repo='', arch=''): if all([name, repo, arch]): + arch_obj = get_object_or_404(Arch, name=arch) + repo_obj = get_object_or_404(Repo, name__iexact=repo) try: pkg = Package.objects.select_related( 'arch', 'repo', 'packager').get(pkgname=name, - repo__name__iexact=repo, arch__name=arch) + repo=repo_obj, arch=arch_obj) return render(request, 'packages/details.html', {'pkg': pkg}) except Package.DoesNotExist: - arch_obj = get_object_or_404(Arch, name=arch) - # for arch='any' packages, we can issue a redirect to them if we - # have a single non-ambiguous option by changing the arch to match - # any arch-agnostic package - if not arch_obj.agnostic: - pkgs = Package.objects.select_related( - 'arch', 'repo', 'packager').filter(pkgname=name, - repo__name__iexact=repo, arch__agnostic=True) - if len(pkgs) == 1: - return redirect(pkgs[0], permanent=True) - # do we have a split package matching this criteria? - ret = split_package_details(request, name, repo, arch) - if ret is None: - # maybe we have a recently-removed package? - ret = recently_removed_package(request, name, repo, arch) - if ret is not None: - return ret - else: - # we've tried everything at this point, nothing to see - raise Http404 + # attempt a variety of fallback options before 404ing + options = (redirect_agnostic, split_package_details, + recently_removed_package) + for method in options: + ret = method(request, name, repo_obj, arch_obj) + if ret: + return ret + # we've tried everything at this point, nothing to see + raise Http404 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.encode('utf-8')) for x, y in pkg_data if y] - return redirect("/packages/?%s" % urlencode(pkg_data)) + return redirect_to_search(request, name, repo, arch) def groups(request, arch=None): -- cgit v1.2.3-2-g168b From 5f410c000eaca4b5b25664f4dfb59cbe85ea034e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 19:12:27 -0500 Subject: Add package details redirect for package replacements This makes sense if there is only one available replacement. We could get more sophisticated and show the removed page if there are multiple replacements available. Additionally, automatically redirect if there was only one matching package for a given package update deletion object. Signed-off-by: Dan McGee --- packages/views/display.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'packages/views') diff --git a/packages/views/display.py b/packages/views/display.py index 1ea9ea48..d6922314 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -51,12 +51,24 @@ def recently_removed_package(request, name, repo, arch, cutoff=CUTOFF): match = match.filter(created__gte=when) try: match = match.latest() + elsewhere = match.elsewhere() + if len(elsewhere) == 1: + return redirect(elsewhere[0]) return render(request, 'packages/removed.html', {'update': match, }, status=410) except Update.DoesNotExist: return None +def replaced_package(request, name, repo, arch): + '''Check our package replacements to see if this is a package we used to + have but no longer do.''' + match = Package.objects.filter(replaces__name=name, repo=repo, arch=arch) + if len(match) == 1: + return redirect(match[0], permanent=True) + return None + + def redirect_agnostic(request, name, repo, arch): '''For arch='any' packages, we can issue a redirect to them if we have a single non-ambiguous option by changing the arch to match any arch-agnostic @@ -95,7 +107,7 @@ def details(request, name='', repo='', arch=''): except Package.DoesNotExist: # attempt a variety of fallback options before 404ing options = (redirect_agnostic, split_package_details, - recently_removed_package) + recently_removed_package, replaced_package) for method in options: ret = method(request, name, repo_obj, arch_obj) if ret: -- cgit v1.2.3-2-g168b From 5f85a1240da14e57760c2ba6585ae943d7a1d8c2 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 19:31:44 -0500 Subject: Reuse removed template for packages with multiple replacements For example, bitcoin-git in the Arch repos is currently marked replaced by both bitcoin-qt and bitcoin-daemon. This allows us to show a page with both options listed instead of a blank 404 page. Signed-off-by: Dan McGee --- packages/views/display.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) (limited to 'packages/views') diff --git a/packages/views/display.py b/packages/views/display.py index d6922314..8adf3bee 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -50,12 +50,19 @@ def recently_removed_package(request, name, repo, arch, cutoff=CUTOFF): when = now() - cutoff match = match.filter(created__gte=when) try: - match = match.latest() - elsewhere = match.elsewhere() + update = match.latest() + elsewhere = update.elsewhere() if len(elsewhere) == 1: return redirect(elsewhere[0]) - return render(request, 'packages/removed.html', - {'update': match, }, status=410) + context = { + 'update': update, + 'elsewhere': elsewhere, + 'name': name, + 'version': update.old_version, + 'arch': arch, + 'repo': repo, + } + return render(request, 'packages/removed.html', context, status=410) except Update.DoesNotExist: return None @@ -66,6 +73,15 @@ def replaced_package(request, name, repo, arch): match = Package.objects.filter(replaces__name=name, repo=repo, arch=arch) if len(match) == 1: return redirect(match[0], permanent=True) + elif len(match) > 1: + context = { + 'elsewhere': match, + 'name': name, + 'version': '', + 'arch': arch, + 'repo': repo, + } + return render(request, 'packages/removed.html', context, status=410) return None -- cgit v1.2.3-2-g168b From f5d3c02eb14ea8b0018e17fa9be9c511ad7ebff9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 19:49:37 -0500 Subject: Revert "Fall back to 410 Gone for package files view as well" This reverts commit 9ab460c53a1ac4c79da6f05f2850ee21beedbab2. This seemed like the right thing to do, but it doesn't really play well with our more general dispatch framework we now do on the package details pages. Just let it 404 like it always did, as these pages are less essential. We can perhaps add a full dispatcher later if we really feel the need. --- packages/views/display.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) (limited to 'packages/views') diff --git a/packages/views/display.py b/packages/views/display.py index 8adf3bee..b5cd643a 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -165,16 +165,8 @@ def group_details(request, arch, name): def files(request, name, repo, arch): - try: - pkg = Package.objects.get(pkgname=name, - repo__name__iexact=repo, arch__name=arch) - except Package.DoesNotExist: - # this may have been deleted recently, so follow the same logic as we - # do on the package details page if possible - ret = recently_removed_package(request, name, repo, arch) - if ret is not None: - return ret - raise Http404 + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) # files are inserted in sorted order, so preserve that fileslist = PackageFile.objects.filter(pkg=pkg).order_by('id') dir_count = sum(1 for f in fileslist if f.is_directory) -- cgit v1.2.3-2-g168b From b425b192e12afd0584bbffc9ff1d997a330bcd5a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 8 Aug 2012 22:21:05 -0500 Subject: Migrate flag request version info to new format Signed-off-by: Dan McGee --- packages/views/flag.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'packages/views') diff --git a/packages/views/flag.py b/packages/views/flag.py index b9542a62..16f5f202 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -59,11 +59,12 @@ def flag(request, name, repo, arch): flagged_pkgs = list(pkgs) # find a common version if there is one available to store - versions = set(pkg.full_version for pkg in flagged_pkgs) + versions = set((pkg.pkgver, pkg.pkgrel, pkg.epoch) + for pkg in flagged_pkgs) if len(versions) == 1: version = versions.pop() else: - version = '' + version = ('', '', 0) message = form.cleaned_data['message'] ip_addr = request.META.get('REMOTE_ADDR') @@ -77,11 +78,12 @@ def flag(request, name, repo, arch): current_time = now() pkgs.update(flag_date=current_time) # store our flag request + # TODO flag_request = FlagRequest(created=current_time, user_email=email, message=message, ip_address=ip_addr, pkgbase=pkg.pkgbase, - version=version, repo=pkg.repo, - num_packages=len(flagged_pkgs)) + repo=pkg.repo, pkgver=version[0], pkgrel=version[1], + epoch=version[2], num_packages=len(flagged_pkgs)) if authenticated: flag_request.user = request.user flag_request.save() -- cgit v1.2.3-2-g168b From 3cb16e4784f492c50555e879ea6b07fd898b1c3d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 13 Aug 2012 09:34:11 -0500 Subject: Attempt to screen for useless out-of-date messages Things like ' ', '-', '.', etc. will no longer be accepted in this field. Signed-off-by: Dan McGee --- packages/views/flag.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'packages/views') diff --git a/packages/views/flag.py b/packages/views/flag.py index 16f5f202..33cec006 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -1,3 +1,5 @@ +import re + from django import forms from django.conf import settings from django.contrib.auth.decorators import permission_required @@ -30,6 +32,15 @@ class FlagForm(forms.Form): if auth: del self.fields['email'] + def clean_message(self): + data = self.cleaned_data['message'] + # make sure the message isn't garbage (only punctuation or whitespace) + # and ensure a certain minimum length + if re.match(r'^[^0-9A-Za-z]+$', data) or len(data) < 3: + raise forms.ValidationError( + "Enter a valid and useful out-of-date message.") + return data + @cache_page(3600) def flaghelp(request): @@ -78,7 +89,6 @@ def flag(request, name, repo, arch): current_time = now() pkgs.update(flag_date=current_time) # store our flag request - # TODO flag_request = FlagRequest(created=current_time, user_email=email, message=message, ip_address=ip_addr, pkgbase=pkg.pkgbase, -- cgit v1.2.3-2-g168b From f7289625000d0f83675fc5a70650b49707338dca Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 20 Aug 2012 21:19:02 -0500 Subject: Use case-insensitive search in opensearch suggestions There is no real good reason not to do this, since our packages are lowercased by convention. Signed-off-by: Dan McGee --- packages/views/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'packages/views') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 038d40ac..f7952255 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -5,6 +5,7 @@ from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import User from django.core.cache import cache +from django.db.models import Q from django.http import HttpResponse from django.shortcuts import redirect, render from django.views.decorators.cache import cache_control @@ -47,8 +48,13 @@ def opensearch_suggest(request): hashlib.md5(search_term.encode('utf-8')).hexdigest() to_json = cache.get(cache_key, None) if to_json is None: - names = Package.objects.filter( - pkgname__startswith=search_term).values_list( + q = Q(pkgname__startswith=search_term) + lookup = search_term.lower() + if search_term != lookup: + # package names are lowercase by convention, so include that in + # search if original wasn't lowercase already + q |= Q(pkgname__startswith=lookup) + names = Package.objects.filter(q).values_list( 'pkgname', flat=True).order_by('pkgname').distinct()[:10] results = [search_term, list(names)] to_json = json.dumps(results, ensure_ascii=False) -- cgit v1.2.3-2-g168b From f2f00b3c0474c4776e7a7f0e58162dc67ce2ca18 Mon Sep 17 00:00:00 2001 From: Florian Pritz Date: Sat, 8 Sep 2012 17:08:21 +0200 Subject: p/v/flag: Add reply-to to out-of-date notifications Signed-off-by: Florian Pritz Signed-off-by: Dan McGee --- packages/views/flag.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'packages/views') diff --git a/packages/views/flag.py b/packages/views/flag.py index 33cec006..d7302f72 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -3,7 +3,7 @@ import re 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.core.mail import send_mail, EmailMessage from django.db import transaction from django.shortcuts import get_object_or_404, redirect, render from django.template import loader, Context @@ -122,11 +122,13 @@ def flag(request, name, repo, arch): 'pkg': pkg, 'packages': flagged_pkgs, }) - send_mail(subject, + msg = EmailMessage(subject, tmpl.render(ctx), 'Arch Website Notification ', toemail, - fail_silently=True) + headers={"Reply-To": email } + ) + msg.send(fail_silently=True) return redirect('package-flag-confirmed', name=name, repo=repo, arch=arch) -- cgit v1.2.3-2-g168b From 10f5c27bf8124a3ccffb94930283b5062ad96cce Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 10 Sep 2012 08:27:23 -0500 Subject: Remove now unnecessary import Signed-off-by: Dan McGee --- packages/views/flag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/views') diff --git a/packages/views/flag.py b/packages/views/flag.py index d7302f72..dadadd19 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -3,7 +3,7 @@ import re from django import forms from django.conf import settings from django.contrib.auth.decorators import permission_required -from django.core.mail import send_mail, EmailMessage +from django.core.mail import EmailMessage from django.db import transaction from django.shortcuts import get_object_or_404, redirect, render from django.template import loader, Context -- cgit v1.2.3-2-g168b From 7a0e6620c9c8782fbef37db15afc3ccebc642d19 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 25 Sep 2012 18:22:16 -0500 Subject: Don't show staging in package search repo listing This is temporary or at least a quick way to ensure regular users aren't confused by staging packages; later updates should re-enable display of this for logged in developers and trusted users. Signed-off-by: Dan McGee --- packages/views/search.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'packages/views') diff --git a/packages/views/search.py b/packages/views/search.py index 497d9bca..1fbe5694 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -56,8 +56,9 @@ class PackageSearchForm(forms.Form): def __init__(self, *args, **kwargs): super(PackageSearchForm, self).__init__(*args, **kwargs) + repos = Repo.objects.filter(staging=False) self.fields['repo'].choices = make_choice( - [repo.name for repo in Repo.objects.all()]) + [repo.name for repo in repos]) self.fields['arch'].choices = make_choice( [arch.name for arch in Arch.objects.all()]) self.fields['q'].widget.attrs.update({"size": "30"}) -- cgit v1.2.3-2-g168b From 44dc458a2b8ca500acda72b437a720dd4f57bedf Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 30 Sep 2012 01:40:25 -0500 Subject: Hide staging packages in search results This is for users that aren't logged in; developers will still see them. Signed-off-by: Dan McGee --- packages/views/search.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'packages/views') diff --git a/packages/views/search.py b/packages/views/search.py index 1fbe5694..99bf703a 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -55,8 +55,11 @@ class PackageSearchForm(forms.Form): initial=50) def __init__(self, *args, **kwargs): + show_staging = kwargs.pop('show_staging', False) super(PackageSearchForm, self).__init__(*args, **kwargs) - repos = Repo.objects.filter(staging=False) + repos = Repo.objects.all() + if not show_staging: + repos = repos.filter(staging=False) self.fields['repo'].choices = make_choice( [repo.name for repo in repos]) self.fields['arch'].choices = make_choice( @@ -129,11 +132,14 @@ class SearchListView(ListView): allowed_sort = list(sort_fields) + ["-" + s for s in sort_fields] def get(self, request, *args, **kwargs): - self.form = PackageSearchForm(data=request.GET) + self.form = PackageSearchForm(data=request.GET, + show_staging=self.request.user.is_authenticated()) return super(SearchListView, self).get(request, *args, **kwargs) def get_queryset(self): packages = Package.objects.normal() + if not self.request.user.is_authenticated(): + packages = packages.filter(repo__staging=False) if self.form.is_valid(): packages = parse_form(self.form, packages) sort = self.form.cleaned_data['sort'] @@ -174,9 +180,12 @@ def search_json(request): } if request.GET: - form = PackageSearchForm(data=request.GET) + form = PackageSearchForm(data=request.GET, + show_staging=request.user.is_authenticated()) if form.is_valid(): packages = Package.objects.normal() + if not request.user.is_authenticated(): + packages = packages.filter(repo__staging=False) packages = parse_form(form, packages)[:limit] container['results'] = packages container['valid'] = True -- cgit v1.2.3-2-g168b From feabc12d384a448614dbc8a9a51cd39ee63b4a83 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 30 Sep 2012 10:11:19 -0500 Subject: =?UTF-8?q?Fix=20usage=20of=20na=C3=AFve=20datetime=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Dan McGee --- packages/views/search.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'packages/views') diff --git a/packages/views/search.py b/packages/views/search.py index 99bf703a..f7b8ed1d 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -1,5 +1,6 @@ from datetime import datetime import json +from pytz import utc from django import forms from django.contrib.admin.widgets import AdminDateWidget @@ -105,8 +106,9 @@ def parse_form(form, packages): 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)) + cutoff = datetime(lu.year, lu.month, lu.day, 0, 0) + cutoff = cutoff.replace(tzinfo=utc) + packages = packages.filter(last_update__gte=cutoff) if form.cleaned_data['name']: name = form.cleaned_data['name'] -- cgit v1.2.3-2-g168b From 2ee662c77cf559d6ea82c9096533abfcd38f4801 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 26 Oct 2012 17:05:39 -0500 Subject: Extract some common architecture grabbing logic Signed-off-by: Dan McGee --- packages/views/display.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'packages/views') diff --git a/packages/views/display.py b/packages/views/display.py index b5cd643a..efedf6ff 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -13,11 +13,16 @@ from ..models import Update from ..utils import get_group_info, PackageJSONEncoder +def arch_plus_agnostic(arch): + arches = [ arch ] + arches.extend(Arch.objects.filter(agnostic=True).order_by()) + return arches + + def split_package_details(request, name, repo, arch): '''Check if we have a split package (e.g. pkgbase) value matching this name. If so, we can show a listing page for the entire set of packages.''' - arches = [ arch ] - arches.extend(Arch.objects.filter(agnostic=True)) + arches = arch_plus_agnostic(arch) pkgs = Package.objects.normal().filter(pkgbase=name, repo__testing=repo.testing, repo__staging=repo.staging, arch__in=arches).order_by('pkgname') @@ -42,8 +47,7 @@ def recently_removed_package(request, name, repo, arch, cutoff=CUTOFF): '''Check our packages update table to see if this package has existed in this repo before. If so, we can show a 410 Gone page and point the requester in the right direction.''' - arches = [ arch ] - arches.extend(Arch.objects.filter(agnostic=True)) + arches = arch_plus_agnostic(arch) match = Update.objects.select_related('arch', 'repo').filter( pkgname=name, repo=repo, arch__in=arches) if cutoff is not None: @@ -149,8 +153,7 @@ def groups(request, arch=None): def group_details(request, arch, name): arch = get_object_or_404(Arch, name=arch) - arches = [ arch ] - arches.extend(Arch.objects.filter(agnostic=True)) + arches = arch_plus_agnostic(arch) pkgs = Package.objects.normal().filter( groups__name=name, arch__in=arches).order_by('pkgname') if len(pkgs) == 0: -- cgit v1.2.3-2-g168b From 6dd4d54bb0adbbb0f8c2b1beaa92b7a58971cf88 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 16 Nov 2012 16:20:11 -0600 Subject: Use Python 2.7 dictionary comprehension syntax Rather than the old idiom of dict((k, v) for <> in <>). Signed-off-by: Dan McGee --- packages/views/signoff.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'packages/views') diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 56eb060c..824a9922 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -155,8 +155,8 @@ class SignoffJSONEncoder(DjangoJSONEncoder): def default(self, obj): if isinstance(obj, PackageSignoffGroup): - data = dict((attr, getattr(obj, attr)) - for attr in self.signoff_group_attrs) + data = {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() @@ -164,9 +164,7 @@ class SignoffJSONEncoder(DjangoJSONEncoder): 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 + return {attr: getattr(obj, attr) for attr in self.signoff_attrs} elif isinstance(obj, Arch) or isinstance(obj, Repo): return unicode(obj) elif isinstance(obj, User): -- cgit v1.2.3-2-g168b From 9e9157d0a8cbf9ea076231e438fb30f58bff8e29 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 16 Nov 2012 16:37:31 -0600 Subject: Use python set comprehension syntax supported in 2.7 Signed-off-by: Dan McGee --- packages/views/signoff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/views') diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 824a9922..340b2311 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -25,7 +25,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)), + 'repo_names': sorted({g.target_repo for g in signoff_groups}), } return render(request, 'packages/signoffs.html', context) -- cgit v1.2.3-2-g168b From 2bfdcec869ed4fceb11b9e0a2777fa53d46fb336 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 12 Jan 2013 16:47:20 -0600 Subject: Make packages JSON search more performant We were peppering the database with a bunch of queries here; using prefetch_related and attach_maintainers can cut down the count significantly. Signed-off-by: Dan McGee --- packages/views/search.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'packages/views') diff --git a/packages/views/search.py b/packages/views/search.py index f7b8ed1d..f6e670df 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -12,7 +12,7 @@ from django.views.generic import ListView from main.models import Package, Arch, Repo from main.utils import make_choice from ..models import PackageRelation -from ..utils import PackageJSONEncoder +from ..utils import attach_maintainers, PackageJSONEncoder def coerce_limit_value(value): @@ -185,10 +185,14 @@ def search_json(request): form = PackageSearchForm(data=request.GET, show_staging=request.user.is_authenticated()) if form.is_valid(): - packages = Package.objects.normal() + packages = Package.objects.select_related('arch', 'repo', + 'packager') if not request.user.is_authenticated(): packages = packages.filter(repo__staging=False) packages = parse_form(form, packages)[:limit] + packages = packages.prefetch_related('groups', 'licenses', + 'conflicts', 'provides', 'replaces', 'depends') + attach_maintainers(packages) container['results'] = packages container['valid'] = True -- cgit v1.2.3-2-g168b From 66850026ca934e5a09238e9033c541cdc5085a42 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 13 Jan 2013 22:34:33 -0600 Subject: Use content_type and not mimetype on HttpResponse() Bug #16519 in Django deprecates mimetype, so update our code accordingly. Signed-off-by: Dan McGee --- packages/views/__init__.py | 4 ++-- packages/views/display.py | 4 ++-- packages/views/search.py | 2 +- packages/views/signoff.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) (limited to 'packages/views') diff --git a/packages/views/__init__.py b/packages/views/__init__.py index f7952255..4c195385 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -42,7 +42,7 @@ def opensearch(request): def opensearch_suggest(request): search_term = request.GET.get('q', '') if search_term == '': - return HttpResponse('', mimetype='application/x-suggestions+json') + return HttpResponse('', content_type='application/x-suggestions+json') cache_key = 'opensearch:packages:' + \ hashlib.md5(search_term.encode('utf-8')).hexdigest() @@ -59,7 +59,7 @@ def opensearch_suggest(request): results = [search_term, list(names)] to_json = json.dumps(results, ensure_ascii=False) cache.set(cache_key, to_json, 300) - return HttpResponse(to_json, mimetype='application/x-suggestions+json') + return HttpResponse(to_json, content_type='application/x-suggestions+json') @permission_required('main.change_package') diff --git a/packages/views/display.py b/packages/views/display.py index efedf6ff..445c1abe 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -188,7 +188,7 @@ def details_json(request, name, repo, arch): pkg = get_object_or_404(Package, pkgname=name, repo__name__iexact=repo, arch__name=arch) to_json = json.dumps(pkg, ensure_ascii=False, cls=PackageJSONEncoder) - return HttpResponse(to_json, mimetype='application/json') + return HttpResponse(to_json, content_type='application/json') def files_json(request, name, repo, arch): @@ -209,7 +209,7 @@ def files_json(request, name, repo, arch): 'files': fileslist, } to_json = json.dumps(data, ensure_ascii=False, cls=PackageJSONEncoder) - return HttpResponse(to_json, mimetype='application/json') + return HttpResponse(to_json, content_type='application/json') def download(request, name, repo, arch): diff --git a/packages/views/search.py b/packages/views/search.py index f6e670df..0f313ccb 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -197,6 +197,6 @@ def search_json(request): container['valid'] = True to_json = json.dumps(container, ensure_ascii=False, cls=PackageJSONEncoder) - return HttpResponse(to_json, mimetype='application/json') + return HttpResponse(to_json, content_type='application/json') # vim: set ts=4 sw=4 et: diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 340b2311..17f3095c 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -67,7 +67,7 @@ def signoff_package(request, name, repo, arch, revoke=False): 'user': str(request.user), } return HttpResponse(json.dumps(data, ensure_ascii=False), - mimetype='application/json') + content_type='application/json') return redirect('package-signoffs') @@ -181,7 +181,7 @@ def signoffs_json(request): 'signoff_groups': signoff_groups, } to_json = json.dumps(data, ensure_ascii=False, cls=SignoffJSONEncoder) - response = HttpResponse(to_json, mimetype='application/json') + response = HttpResponse(to_json, content_type='application/json') return response # vim: set ts=4 sw=4 et: -- cgit v1.2.3-2-g168b From a10798b756bbfc5d8dbad76546ca670efca75e56 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 23 Jan 2013 09:18:59 -0700 Subject: Use querysets for calls to get_object_or_404(Package) This works better in most cases since we need the architecture and repository objects at some point during the view process. Signed-off-by: Dan McGee --- packages/views/display.py | 8 ++++---- packages/views/flag.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) (limited to 'packages/views') diff --git a/packages/views/display.py b/packages/views/display.py index 445c1abe..c2369aba 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -168,7 +168,7 @@ def group_details(request, arch, name): def files(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) # files are inserted in sorted order, so preserve that fileslist = PackageFile.objects.filter(pkg=pkg).order_by('id') @@ -185,14 +185,14 @@ def files(request, name, repo, arch): def details_json(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) to_json = json.dumps(pkg, ensure_ascii=False, cls=PackageJSONEncoder) return HttpResponse(to_json, content_type='application/json') def files_json(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) # files are inserted in sorted order, so preserve that fileslist = PackageFile.objects.filter(pkg=pkg).order_by('id') @@ -213,7 +213,7 @@ def files_json(request, name, repo, arch): def download(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) url = get_mirror_url_for_download() if not url: diff --git a/packages/views/flag.py b/packages/views/flag.py index dadadd19..edb3f092 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -49,7 +49,7 @@ def flaghelp(request): @never_cache def flag(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) if pkg.flag_date is not None: # already flagged. do nothing. @@ -158,7 +158,7 @@ def flag_confirmed(request, name, repo, arch): @permission_required('main.change_package') def unflag(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) pkg.flag_date = None pkg.save() @@ -166,7 +166,7 @@ def unflag(request, name, repo, arch): @permission_required('main.change_package') def unflag_all(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) # find all packages from (hopefully) the same PKGBUILD pkgs = Package.objects.filter(pkgbase=pkg.pkgbase, -- cgit v1.2.3-2-g168b From dc6cc49f6f876983f76f5f8c05a2285801f27ea0 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 23 Jan 2013 09:20:19 -0700 Subject: Use more modern verison of string template formatting Signed-off-by: Dan McGee --- packages/views/display.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'packages/views') diff --git a/packages/views/display.py b/packages/views/display.py index c2369aba..497c8d48 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -1,6 +1,5 @@ import datetime import json -from string import Template from urllib import urlencode from django.http import HttpResponse, Http404 @@ -223,12 +222,9 @@ def download(request, name, repo, arch): # grab the first non-any arch to fake the download path arch = Arch.objects.exclude(agnostic=True)[0].name values = { - 'host': url.url, - 'arch': arch, - 'repo': pkg.repo.name.lower(), - 'file': pkg.filename, } - url = Template('${host}${repo}/os/${arch}/${file}').substitute(values) + url = '{host}{repo}/os/{arch}/{filename}'.format(host=url.url, + repo=pkg.repo.name.lower(), arch=arch, filename=pkg.filename) return redirect(url) # vim: set ts=4 sw=4 et: -- cgit v1.2.3-2-g168b From e65c7805547484cad1be55dfa20355ef18b857be Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 8 Feb 2013 21:09:47 -0600 Subject: Remove package seach by 'Last Updated After' It is a lot easier to just sort the list rather than mess with this particular field, which didn't even allow you to specify a range or direction to search in. Signed-off-by: Dan McGee --- packages/views/search.py | 9 --------- 1 file changed, 9 deletions(-) (limited to 'packages/views') diff --git a/packages/views/search.py b/packages/views/search.py index 0f313ccb..9cb5f38d 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -3,7 +3,6 @@ import json from pytz import utc 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.http import HttpResponse @@ -44,8 +43,6 @@ class PackageSearchForm(forms.Form): sort = forms.CharField(required=False, widget=forms.HiddenInput()) 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) @@ -104,12 +101,6 @@ def parse_form(form, packages): elif form.cleaned_data['flagged'] == 'Not Flagged': packages = packages.filter(flag_date__isnull=True) - if form.cleaned_data['last_update']: - lu = form.cleaned_data['last_update'] - cutoff = datetime(lu.year, lu.month, lu.day, 0, 0) - cutoff = cutoff.replace(tzinfo=utc) - packages = packages.filter(last_update__gte=cutoff) - if form.cleaned_data['name']: name = form.cleaned_data['name'] packages = packages.filter(pkgname=name) -- cgit v1.2.3-2-g168b From 5566d43a7734f6bb2f48d5d511351da12ddc5cc1 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 9 Feb 2013 16:43:40 -0600 Subject: Use 'update_fields' model.save() kwarg This was added in Django 1.5 and allows saving only a subset of a model's fields. It makes sense in a few cases to utilize it. Signed-off-by: Dan McGee --- packages/views/signoff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/views') diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 17f3095c..c37aa0fc 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -45,7 +45,7 @@ def signoff_package(request, name, repo, arch, revoke=False): except Signoff.DoesNotExist: raise Http404 signoff.revoked = now() - signoff.save() + signoff.save(update_fields=('revoked',)) created = False else: # ensure we should even be accepting signoffs -- cgit v1.2.3-2-g168b From e3837b5a872b6203b7ae338bc8075b339e031627 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 16 Feb 2013 15:23:49 -0600 Subject: Remove configurable pagination for package search Switch it to a hardcoded value of 100 for all searches instead. It didn't make much sense having a page number be part of the URL and a limit value being part of the query string. Signed-off-by: Dan McGee --- packages/views/search.py | 38 +------------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) (limited to 'packages/views') diff --git a/packages/views/search.py b/packages/views/search.py index 9cb5f38d..9ca299f6 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -1,6 +1,4 @@ -from datetime import datetime import json -from pytz import utc from django import forms from django.contrib.auth.models import User @@ -14,26 +12,6 @@ from ..models import PackageRelation from ..utils import attach_maintainers, PackageJSONEncoder -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) @@ -46,11 +24,6 @@ class PackageSearchForm(forms.Form): flagged = forms.ChoiceField( choices=[('', 'All')] + make_choice(['Flagged', 'Not Flagged']), 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): show_staging = kwargs.pop('show_staging', False) @@ -119,6 +92,7 @@ def parse_form(form, packages): class SearchListView(ListView): template_name = "packages/search.html" + paginate_by = 100 sort_fields = ("arch", "repo", "pkgname", "pkgbase", "compressed_size", "installed_size", "build_date", "last_update", "flag_date") @@ -145,16 +119,6 @@ class SearchListView(ListView): # Form had errors so don't return any results return Package.objects.none() - def get_paginate_by(self, queryset): - limit = 50 - if self.form.is_valid(): - asked_limit = self.form.cleaned_data['limit'] - if asked_limit and asked_limit < 0: - limit = None - elif asked_limit: - limit = asked_limit - return limit - def get_context_data(self, **kwargs): context = super(SearchListView, self).get_context_data(**kwargs) context['current_query'] = self.request.GET.urlencode() -- cgit v1.2.3-2-g168b From 746023d529489b68f1a2494ff7572734b3b368ce Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 16 Feb 2013 15:38:29 -0600 Subject: Make page a query string parameter on package search This is a bit silly to encode in the URL, or at least makes it much harder to screen out via robots.txt and other such things. Signed-off-by: Dan McGee --- packages/views/display.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'packages/views') diff --git a/packages/views/display.py b/packages/views/display.py index 497c8d48..fcf8fdea 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -104,6 +104,8 @@ def redirect_agnostic(request, name, repo, arch): def redirect_to_search(request, name, repo, arch): + if request.GET.get('q'): + name = request.GET.get('q') pkg_data = [ ('arch', arch.lower()), ('repo', repo.lower()), -- cgit v1.2.3-2-g168b From 0491bdb2452e496b0a243e7abb3d15ef3fd71743 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 20 Feb 2013 00:19:12 -0600 Subject: Fix some fallout with moving page to query params Signed-off-by: Dan McGee --- packages/views/search.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'packages/views') diff --git a/packages/views/search.py b/packages/views/search.py index 9ca299f6..0362602e 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -121,7 +121,9 @@ class SearchListView(ListView): def get_context_data(self, **kwargs): context = super(SearchListView, self).get_context_data(**kwargs) - context['current_query'] = self.request.GET.urlencode() + query_params = self.request.GET.copy() + query_params.pop('page', None) + context['current_query'] = query_params.urlencode() context['search_form'] = self.form return context -- cgit v1.2.3-2-g168b From dd0ecfaeaceb1e1b8a185800de35f0f6e741feac Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 26 Feb 2013 19:51:40 -0600 Subject: Use user.userprofile rather than user.get_profile() The get_profile() function is deprecated as of Django 1.5. Signed-off-by: Dan McGee --- packages/views/flag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/views') diff --git a/packages/views/flag.py b/packages/views/flag.py index edb3f092..5c76e1d5 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -110,7 +110,7 @@ def flag(request, name, repo, arch): subject = '%s package [%s] marked out-of-date' % \ (pkg.repo.name, pkg.pkgname) for maint in maints: - if maint.get_profile().notify == True: + if maint.userprofile.notify == True: toemail.append(maint.email) if toemail: -- cgit v1.2.3-2-g168b