summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README9
-rw-r--r--devel/management/commands/reporead.py6
-rw-r--r--devel/urls.py9
-rw-r--r--devel/views.py142
-rw-r--r--feeds.py99
-rw-r--r--main/admin.py6
-rw-r--r--main/fixtures/groups.json218
-rw-r--r--main/fixtures/repos.json56
-rw-r--r--main/migrations/0047_utc_datetimes.py180
-rw-r--r--main/migrations/0048_auto__add_field_repo_bugs_category.py158
-rw-r--r--main/models.py55
-rw-r--r--main/templatetags/attributes.py21
-rw-r--r--main/utils.py34
-rw-r--r--media/archweb.css5
-rw-r--r--media/archweb.js30
-rw-r--r--mirrors/admin.py2
-rw-r--r--mirrors/management/commands/mirrorcheck.py30
-rw-r--r--mirrors/management/commands/mirrorresolv.py2
-rw-r--r--mirrors/migrations/0007_unique_names_urls.py66
-rw-r--r--mirrors/migrations/0008_auto__add_field_mirrorurl_country.py67
-rw-r--r--mirrors/models.py14
-rw-r--r--mirrors/utils.py51
-rw-r--r--mirrors/views.py32
-rw-r--r--news/migrations/0007_add_guid.py65
-rw-r--r--news/migrations/0008_set_prior_guids.py83
-rw-r--r--news/migrations/0009_utc_datetimes.py85
-rw-r--r--news/models.py31
-rw-r--r--packages/migrations/0007_auto__add_field_packagerelation_created.py135
-rw-r--r--packages/models.py13
-rw-r--r--packages/templatetags/package_extras.py37
-rw-r--r--packages/urls.py5
-rw-r--r--packages/views.py64
-rw-r--r--public/utils.py75
-rw-r--r--releng/__init__.py0
-rw-r--r--releng/admin.py31
-rw-r--r--releng/fixtures/architecture.json30
-rw-r--r--releng/fixtures/bootloaders.json23
-rw-r--r--releng/fixtures/boottype.json23
-rw-r--r--releng/fixtures/clockchoices.json23
-rw-r--r--releng/fixtures/filesystems.json23
-rw-r--r--releng/fixtures/hardware.json44
-rw-r--r--releng/fixtures/installtype.json30
-rw-r--r--releng/fixtures/isotypes.json16
-rw-r--r--releng/fixtures/modules.json86
-rw-r--r--releng/fixtures/source.json23
-rw-r--r--releng/management/__init__.py0
-rw-r--r--releng/management/commands/__init__.py0
-rw-r--r--releng/management/commands/syncisos.py51
-rw-r--r--releng/migrations/0001_initial.py258
-rw-r--r--releng/migrations/__init__.py0
-rw-r--r--releng/models.py120
-rw-r--r--releng/urls.py14
-rw-r--r--releng/views.py136
-rw-r--r--requirements.txt4
-rw-r--r--requirements_prod.txt6
-rw-r--r--settings.py4
-rw-r--r--templates/devel/index.html50
-rw-r--r--templates/devel/packages.html58
-rw-r--r--templates/feeds/news_description.html2
-rw-r--r--templates/feeds/news_title.html2
-rw-r--r--templates/feeds/packages_description.html2
-rw-r--r--templates/feeds/packages_title.html2
-rw-r--r--templates/mirrors/mirror_details.html111
-rw-r--r--templates/mirrors/mirrorlist.txt2
-rw-r--r--templates/mirrors/mirrorlist_status.txt2
-rw-r--r--templates/mirrors/mirrors.html2
-rw-r--r--templates/mirrors/status.html9
-rw-r--r--templates/mirrors/status_table.html2
-rw-r--r--templates/packages/details.html9
-rw-r--r--templates/packages/flag.html28
-rw-r--r--templates/packages/flag_confirmed.html19
-rw-r--r--templates/packages/outofdate.txt8
-rw-r--r--templates/packages/stale_relations.html6
-rw-r--r--templates/public/index.html16
-rw-r--r--templates/releng/add.html27
-rw-r--r--templates/releng/result_list.html41
-rw-r--r--templates/releng/result_section.html28
-rw-r--r--templates/releng/results.html29
-rw-r--r--templates/releng/thanks.html13
-rw-r--r--templates/todolists/email_notification.txt2
-rw-r--r--todolists/urls.py9
-rw-r--r--todolists/utils.py19
-rw-r--r--todolists/views.py45
-rw-r--r--urls.py18
84 files changed, 2998 insertions, 393 deletions
diff --git a/README b/README
index 3e315388..184d1c8a 100644
--- a/README
+++ b/README
@@ -54,12 +54,10 @@ packages, you will probably want the following:
(archweb-env) $ ./manage.py migrate
-6. Load the fixtures to prepopulate some data.
+6. Load the fixtures to prepopulate some data. If you don't want some of the
+ provided data, adjust the file glob accordingly.
- (archweb-env) $ ./manage.py loaddata main/fixtures/arches.json
- (archweb-env) $ ./manage.py loaddata main/fixtures/repos.json
- (archweb-env) $ ./manage.py loaddata main/fixtures/groups.json
- (archweb-env) $ ./manage.py loaddata mirrors/fixtures/mirrorprotocols.json
+ (archweb-env) $ ./manage.py loaddata */fixtures/*.json
7. Use the following commands to start a service instance
@@ -69,6 +67,7 @@ packages, you will probably want the following:
(archweb-env) $ wget ftp://ftp.archlinux.org/core/os/i686/core.db.tar.gz
(archweb-env) $ ./manage.py reporead i686 core.db.tar.gz
+ (archweb-env) $ ./manage.py syncisos
Alter architecture and repo to get x86\_64 and packages from other repos if needed.
diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py
index e26bb800..a8875c7e 100644
--- a/devel/management/commands/reporead.py
+++ b/devel/management/commands/reporead.py
@@ -315,7 +315,7 @@ def populate_files(dbpkg, repopkg, force=False):
directory=dirname + '/',
filename=filename)
pkgfile.save(force_insert=True)
- dbpkg.files_last_update = datetime.now()
+ dbpkg.files_last_update = datetime.utcnow()
dbpkg.save()
@transaction.commit_on_success
@@ -374,7 +374,7 @@ def db_update(archname, reponame, pkgs, options):
for p in [x for x in pkgs if x.name in in_sync_not_db]:
logger.info("Adding package %s", p.name)
pkg = Package(pkgname = p.name, arch = architecture, repo = repository)
- populate_pkg(pkg, p, timestamp=datetime.now())
+ populate_pkg(pkg, p, timestamp=datetime.utcnow())
# packages in database and not in syncdb (remove from database)
in_db_not_sync = dbset - syncset
@@ -398,7 +398,7 @@ def db_update(archname, reponame, pkgs, options):
if not force:
continue
else:
- timestamp = datetime.now()
+ timestamp = datetime.utcnow()
if filesonly:
logger.debug("Checking files for package %s in database", p.name)
populate_files(dbp, p, force=force)
diff --git a/devel/urls.py b/devel/urls.py
index 41be2b31..9bf50f45 100644
--- a/devel/urls.py
+++ b/devel/urls.py
@@ -1,12 +1,13 @@
from django.conf.urls.defaults import patterns
urlpatterns = patterns('devel.views',
- (r'^$', 'index'),
+ (r'^admin_log/$','admin_log'),
+ (r'^admin_log/(?P<username>.*)/$','admin_log'),
(r'^clock/$', 'clock'),
- (r'^profile/$', 'change_profile'),
+ (r'^$', 'index'),
(r'^newuser/$', 'new_user_form'),
- (r'^admin_log/(?P<username>.*)/$','admin_log'),
- (r'^admin_log/$','admin_log'),
+ (r'^profile/$', 'change_profile'),
+ (r'^reports/(?P<report>.*)/$', 'report'),
)
# vim: set ts=4 sw=4 et:
diff --git a/devel/views.py b/devel/views.py
index 5b03f8c0..5d34cc41 100644
--- a/devel/views.py
+++ b/devel/views.py
@@ -2,21 +2,27 @@ from django import forms
from django.http import HttpResponseRedirect
from django.contrib.auth.decorators import \
login_required, permission_required, user_passes_test
-from django.contrib.auth.models import User
+from django.contrib.auth.models import User, Group
from django.contrib.sites.models import Site
from django.core.mail import send_mail
+from django.db import transaction
+from django.db.models import Q
+from django.http import Http404
from django.shortcuts import get_object_or_404
from django.template import loader, Context
+from django.template.defaultfilters import filesizeformat
from django.views.decorators.cache import never_cache
from django.views.generic.simple import direct_to_template
-from main.models import Package, Todolist, TodolistPkg
+from main.models import Package, PackageDepend, PackageFile, TodolistPkg
from main.models import Arch, Repo
from main.models import UserProfile
from packages.models import PackageRelation
+from todolists.utils import get_annotated_todolists
from .utils import get_annotated_maintainers
-import datetime
+from datetime import datetime, timedelta
+import operator
import pytz
import random
from string import ascii_letters, digits
@@ -24,7 +30,7 @@ from string import ascii_letters, digits
@login_required
@never_cache
def index(request):
- '''the Developer dashboard'''
+ '''the developer dashboard'''
inner_q = PackageRelation.objects.filter(user=request.user).values('pkgbase')
flagged = Package.objects.select_related('arch', 'repo').filter(
flag_date__isnull=False, pkgbase__in=inner_q).order_by('pkgname')
@@ -34,6 +40,9 @@ def index(request):
todopkgs = todopkgs.filter(pkg__pkgbase__in=inner_q).order_by(
'list__name', 'pkg__pkgname')
+ todolists = get_annotated_todolists()
+ todolists = [todolist for todolist in todolists if todolist.incomplete_count > 0]
+
maintainers = get_annotated_maintainers()
maintained = PackageRelation.objects.filter(
@@ -47,7 +56,7 @@ def index(request):
}
page_dict = {
- 'todos': Todolist.objects.incomplete().order_by('-date_added'),
+ 'todos': todolists,
'repos': Repo.objects.all(),
'arches': Arch.objects.all(),
'maintainers': maintainers,
@@ -65,8 +74,8 @@ def clock(request):
'username').select_related('userprofile')
# now annotate each dev object with their current time
- now = datetime.datetime.now()
- utc_now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
+ now = datetime.now()
+ utc_now = datetime.utcnow().replace(tzinfo=pytz.utc)
for dev in devs:
# Work around https://bugs.launchpad.net/pytz/+bug/718673
timezone = str(dev.userprofile.time_zone)
@@ -118,14 +127,84 @@ def change_profile(request):
return direct_to_template(request, 'devel/profile.html',
{'form': form, 'profile_form': profile_form})
+@login_required
+def report(request, report):
+ title = 'Developer Report'
+ packages = Package.objects.select_related('arch', 'repo')
+ names = attrs = None
+ if report == 'old':
+ title = 'Packages last built more than two years ago'
+ cutoff = datetime.now() - timedelta(days=730)
+ packages = packages.filter(build_date__lt=cutoff).order_by('build_date')
+ elif report == 'big':
+ title = 'Packages with compressed size > 50 MiB'
+ cutoff = 50 * 1024 * 1024
+ packages = packages.filter(compressed_size__gte=cutoff).order_by('-compressed_size')
+ names = [ 'Compressed Size', 'Installed Size' ]
+ attrs = [ 'compressed_size_pretty', 'installed_size_pretty' ]
+ # Format the compressed and installed sizes with MB/GB/etc suffixes
+ for package in packages:
+ package.compressed_size_pretty = filesizeformat(
+ package.compressed_size)
+ package.installed_size_pretty = filesizeformat(
+ package.installed_size)
+ elif report == 'uncompressed-man':
+ title = 'Packages with uncompressed manpages'
+ # magic going on here! Checking for all '.1'...'.9' extensions
+ invalid_endings = [Q(filename__endswith='.%d' % n) for n in range(1,10)]
+ invalid_endings.append(Q(filename__endswith='.n'))
+ bad_files = PackageFile.objects.filter(Q(directory__contains='man') & (
+ reduce(operator.or_, invalid_endings))
+ ).values_list('pkg_id', flat=True).distinct()
+ packages = packages.filter(id__in=set(bad_files))
+ elif report == 'uncompressed-info':
+ title = 'Packages with uncompressed infopages'
+ # we don't worry abut looking for '*.info-1', etc., given that an
+ # uncompressed root page probably exists in the package anyway
+ bad_files = PackageFile.objects.filter(directory__endswith='/info/',
+ filename__endswith='.info').values_list(
+ 'pkg_id', flat=True).distinct()
+ packages = packages.filter(id__in=set(bad_files))
+ elif report == 'unneeded-orphans':
+ title = 'Orphan packages required by no other packages'
+ owned = PackageRelation.objects.all().values('pkgbase')
+ required = PackageDepend.objects.all().values('depname')
+ # The two separate calls to exclude is required to do the right thing
+ packages = packages.exclude(pkgbase__in=owned).exclude(
+ pkgname__in=required)
+ else:
+ raise Http404
+
+ context = {
+ 'title': title,
+ 'packages': packages,
+ 'column_names': names,
+ 'column_attrs': attrs,
+ }
+ return direct_to_template(request, 'devel/packages.html', context)
+
+
class NewUserForm(forms.ModelForm):
- class Meta:
- model = UserProfile
- exclude = ('picture', 'user')
username = forms.CharField(max_length=30)
- email = forms.EmailField()
+ private_email = forms.EmailField()
first_name = forms.CharField(required=False)
last_name = forms.CharField(required=False)
+ groups = forms.ModelMultipleChoiceField(required=False,
+ queryset=Group.objects.all())
+
+ class Meta:
+ model = UserProfile
+ exclude = ('picture', 'user')
+
+ def __init__(self, *args, **kwargs):
+ super(NewUserForm, self).__init__(*args, **kwargs)
+ # Hack ourself so certain fields appear first. self.fields is a
+ # SortedDict object where we can manipulate the keyOrder list.
+ order = self.fields.keyOrder
+ keys = ('username', 'private_email', 'first_name', 'last_name')
+ for key in reversed(keys):
+ order.remove(key)
+ order.insert(0, key)
def clean_username(self):
username = self.cleaned_data['username']
@@ -134,38 +213,61 @@ class NewUserForm(forms.ModelForm):
"A user with that username already exists.")
return username
- def save(self):
- profile = forms.ModelForm.save(self, False)
+ def save(self, commit=True):
+ profile = super(NewUserForm, self).save(False)
pwletters = ascii_letters + digits
password = ''.join([random.choice(pwletters) for i in xrange(8)])
user = User.objects.create_user(username=self.cleaned_data['username'],
- email=self.cleaned_data['email'], password=password)
+ email=self.cleaned_data['private_email'], password=password)
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
user.save()
+ # sucks that the MRM.add() method can't take a list directly... we have
+ # to resort to dirty * magic.
+ user.groups.add(*self.cleaned_data['groups'])
profile.user = user
- profile.save()
+ if commit:
+ profile.save()
+ self.save_m2m()
- t = loader.get_template('devel/new_account.txt')
- c = Context({
+ template = loader.get_template('devel/new_account.txt')
+ ctx = Context({
'site': Site.objects.get_current(),
'user': user,
'password': password,
})
- send_mail("Your new archweb account",
- t.render(c),
+ send_mail("Your new parabolaweb account",
+ template.render(ctx),
'Parabola <dev@list.parabolagnulinux.org>',
[user.email],
fail_silently=False)
+def log_addition(request, obj):
+ """Cribbed from ModelAdmin.log_addition."""
+ from django.contrib.admin.models import LogEntry, ADDITION
+ from django.contrib.contenttypes.models import ContentType
+ from django.utils.encoding import force_unicode
+ LogEntry.objects.log_action(
+ user_id = request.user.pk,
+ content_type_id = ContentType.objects.get_for_model(obj).pk,
+ object_id = obj.pk,
+ object_repr = force_unicode(obj),
+ action_flag = ADDITION,
+ change_message = "Added via Create New User form."
+ )
+
@permission_required('auth.add_user')
@never_cache
def new_user_form(request):
if request.POST:
form = NewUserForm(request.POST)
if form.is_valid():
- form.save()
+ @transaction.commit_on_success
+ def inner_save():
+ form.save()
+ log_addition(request, form.instance.user)
+ inner_save()
return HttpResponseRedirect('/admin/auth/user/%d/' % \
form.instance.user.id)
else:
diff --git a/feeds.py b/feeds.py
index bff97cba..65d11684 100644
--- a/feeds.py
+++ b/feeds.py
@@ -1,57 +1,46 @@
-import datetime
-from decimal import Decimal, ROUND_HALF_DOWN
+import pytz
+from django.contrib.sites.models import Site
from django.contrib.syndication.views import Feed
-from django.core.cache import cache
from django.db.models import Q
+from django.utils.feedgenerator import Rss201rev2Feed
from django.utils.hashcompat import md5_constructor
from django.views.decorators.http import condition
+from main.utils import retrieve_latest
from main.models import Arch, Repo, Package
-from main.utils import CACHE_TIMEOUT, INVALIDATE_TIMEOUT
-from main.utils import CACHE_PACKAGE_KEY, CACHE_NEWS_KEY
from news.models import News
-def utc_offset():
- '''Calculate the UTC offset from local time. Useful for converting values
- stored in local time to things like cache last modifed headers.'''
- timediff = datetime.datetime.utcnow() - datetime.datetime.now()
- secs = timediff.days * 86400 + timediff.seconds
- # round to nearest minute
- mins = Decimal(secs) / Decimal(60)
- mins = mins.quantize(Decimal('0'), rounding=ROUND_HALF_DOWN)
- return datetime.timedelta(minutes=int(mins))
-
-
-def retrieve_package_latest():
- # we could break this down based on the request url, but it would probably
- # cost us more in query time to do so.
- latest = cache.get(CACHE_PACKAGE_KEY)
- if latest:
- return latest
- try:
- latest = Package.objects.values('last_update').latest(
- 'last_update')['last_update']
- latest = latest + utc_offset()
- # Using add means "don't overwrite anything in there". What could be in
- # there is an explicit None value that our refresh signal set, which
- # means we want to avoid race condition possibilities for a bit.
- cache.add(CACHE_PACKAGE_KEY, latest, CACHE_TIMEOUT)
- return latest
- except Package.DoesNotExist:
- pass
- return None
+def check_for_unique_id(f):
+ def wrapper(name, contents=None, attrs=None):
+ if attrs is None:
+ attrs = {}
+ if name == 'guid':
+ attrs['isPermaLink'] = 'false'
+ return f(name, contents, attrs)
+ return wrapper
+
+class GuidNotPermalinkFeed(Rss201rev2Feed):
+ def write_items(self, handler):
+ # Totally disgusting. Monkey-patch the hander so if it sees a
+ # 'unique-id' field come through, add an isPermalink="false" attribute.
+ # Workaround for http://code.djangoproject.com/ticket/9800
+ handler.addQuickElement = check_for_unique_id(handler.addQuickElement)
+ super(GuidNotPermalinkFeed, self).write_items(handler)
+
def package_etag(request, *args, **kwargs):
- latest = retrieve_package_latest()
+ latest = retrieve_latest(Package)
if latest:
return md5_constructor(str(kwargs) + str(latest)).hexdigest()
return None
def package_last_modified(request, *args, **kwargs):
- return retrieve_package_latest()
+ return retrieve_latest(Package)
class PackageFeed(Feed):
+ feed_type = GuidNotPermalinkFeed
+
link = '/packages/'
title_template = 'feeds/packages_title.html'
description_template = 'feeds/packages_description.html'
@@ -97,44 +86,41 @@ class PackageFeed(Feed):
s += '.'
return s
+ subtitle = description
+
def items(self, obj):
return obj['qs']
+ def item_guid(self, item):
+ # http://diveintomark.org/archives/2004/05/28/howto-atom-id
+ date = item.last_update
+ return 'tag:%s,%s:%s%s' % (Site.objects.get_current().domain,
+ date.strftime('%Y-%m-%d'), item.get_absolute_url(),
+ date.strftime('%Y%m%d%H%M'))
+
def item_pubdate(self, item):
- return item.last_update
+ return item.last_update.replace(tzinfo=pytz.utc)
def item_categories(self, item):
return (item.repo.name, item.arch.name)
-def retrieve_news_latest():
- latest = cache.get(CACHE_NEWS_KEY)
- if latest:
- return latest
- try:
- latest = News.objects.values('last_modified').latest(
- 'last_modified')['last_modified']
- latest = latest + utc_offset()
- # same thoughts apply as in retrieve_package_latest
- cache.add(CACHE_NEWS_KEY, latest, CACHE_TIMEOUT)
- return latest
- except News.DoesNotExist:
- pass
- return None
-
def news_etag(request, *args, **kwargs):
- latest = retrieve_news_latest()
+ latest = retrieve_latest(News)
if latest:
return md5_constructor(str(latest)).hexdigest()
return None
def news_last_modified(request, *args, **kwargs):
- return retrieve_news_latest()
+ return retrieve_latest(News)
class NewsFeed(Feed):
+ feed_type = GuidNotPermalinkFeed
+
title = 'Parabola GNU/Linux-libre: Recent news updates'
link = '/news/'
description = 'The latest news from the Parabola GNU/Linux-libre distribution.'
+ subtitle = description
title_template = 'feeds/news_title.html'
description_template = 'feeds/news_description.html'
@@ -146,8 +132,11 @@ class NewsFeed(Feed):
return News.objects.select_related('author').order_by(
'-postdate', '-id')[:10]
+ def item_guid(self, item):
+ return item.guid
+
def item_pubdate(self, item):
- return item.postdate
+ return item.postdate.replace(tzinfo=pytz.utc)
def item_author_name(self, item):
return item.author.get_full_name()
diff --git a/main/admin.py b/main/admin.py
index 45bc5ab2..e86e5cab 100644
--- a/main/admin.py
+++ b/main/admin.py
@@ -14,12 +14,14 @@ class ArchAdmin(admin.ModelAdmin):
search_fields = ('name',)
class RepoAdmin(admin.ModelAdmin):
- list_display = ('name', 'testing', 'staging', 'bugs_project', 'svn_root')
+ list_display = ('name', 'testing', 'staging', 'bugs_project',
+ 'bugs_category', 'svn_root')
list_filter = ('testing', 'staging')
search_fields = ('name',)
class PackageAdmin(admin.ModelAdmin):
- list_display = ('pkgname', 'repo', 'arch', 'last_update')
+ list_display = ('pkgname', 'full_version', 'repo', 'arch', 'packager',
+ 'last_update', 'build_date')
list_filter = ('repo', 'arch')
search_fields = ('pkgname',)
diff --git a/main/fixtures/groups.json b/main/fixtures/groups.json
index 32416a7a..8a6b2287 100644
--- a/main/fixtures/groups.json
+++ b/main/fixtures/groups.json
@@ -85,11 +85,6 @@
"mirrorprotocol"
],
[
- "delete_mirrorprotocol",
- "mirrors",
- "mirrorprotocol"
- ],
- [
"add_mirrorrsync",
"mirrors",
"mirrorrsync"
@@ -123,6 +118,219 @@
}
},
{
+ "pk": 6,
+ "model": "auth.group",
+ "fields": {
+ "name": "Package Relation Maintainers",
+ "permissions": [
+ [
+ "add_packagerelation",
+ "packages",
+ "packagerelation"
+ ],
+ [
+ "change_packagerelation",
+ "packages",
+ "packagerelation"
+ ],
+ [
+ "delete_packagerelation",
+ "packages",
+ "packagerelation"
+ ]
+ ]
+ }
+ },
+ {
+ "pk": 5,
+ "model": "auth.group",
+ "fields": {
+ "name": "Release Engineering",
+ "permissions": [
+ [
+ "add_architecture",
+ "releng",
+ "architecture"
+ ],
+ [
+ "change_architecture",
+ "releng",
+ "architecture"
+ ],
+ [
+ "delete_architecture",
+ "releng",
+ "architecture"
+ ],
+ [
+ "add_bootloader",
+ "releng",
+ "bootloader"
+ ],
+ [
+ "change_bootloader",
+ "releng",
+ "bootloader"
+ ],
+ [
+ "delete_bootloader",
+ "releng",
+ "bootloader"
+ ],
+ [
+ "add_boottype",
+ "releng",
+ "boottype"
+ ],
+ [
+ "change_boottype",
+ "releng",
+ "boottype"
+ ],
+ [
+ "delete_boottype",
+ "releng",
+ "boottype"
+ ],
+ [
+ "add_clockchoice",
+ "releng",
+ "clockchoice"
+ ],
+ [
+ "change_clockchoice",
+ "releng",
+ "clockchoice"
+ ],
+ [
+ "delete_clockchoice",
+ "releng",
+ "clockchoice"
+ ],
+ [
+ "add_filesystem",
+ "releng",
+ "filesystem"
+ ],
+ [
+ "change_filesystem",
+ "releng",
+ "filesystem"
+ ],
+ [
+ "delete_filesystem",
+ "releng",
+ "filesystem"
+ ],
+ [
+ "add_hardwaretype",
+ "releng",
+ "hardwaretype"
+ ],
+ [
+ "change_hardwaretype",
+ "releng",
+ "hardwaretype"
+ ],
+ [
+ "delete_hardwaretype",
+ "releng",
+ "hardwaretype"
+ ],
+ [
+ "add_installtype",
+ "releng",
+ "installtype"
+ ],
+ [
+ "change_installtype",
+ "releng",
+ "installtype"
+ ],
+ [
+ "delete_installtype",
+ "releng",
+ "installtype"
+ ],
+ [
+ "add_iso",
+ "releng",
+ "iso"
+ ],
+ [
+ "change_iso",
+ "releng",
+ "iso"
+ ],
+ [
+ "delete_iso",
+ "releng",
+ "iso"
+ ],
+ [
+ "add_isotype",
+ "releng",
+ "isotype"
+ ],
+ [
+ "change_isotype",
+ "releng",
+ "isotype"
+ ],
+ [
+ "delete_isotype",
+ "releng",
+ "isotype"
+ ],
+ [
+ "add_module",
+ "releng",
+ "module"
+ ],
+ [
+ "change_module",
+ "releng",
+ "module"
+ ],
+ [
+ "delete_module",
+ "releng",
+ "module"
+ ],
+ [
+ "add_source",
+ "releng",
+ "source"
+ ],
+ [
+ "change_source",
+ "releng",
+ "source"
+ ],
+ [
+ "delete_source",
+ "releng",
+ "source"
+ ],
+ [
+ "add_test",
+ "releng",
+ "test"
+ ],
+ [
+ "change_test",
+ "releng",
+ "test"
+ ],
+ [
+ "delete_test",
+ "releng",
+ "test"
+ ]
+ ]
+ }
+ },
+ {
"pk": 2,
"model": "auth.group",
"fields": {
diff --git a/main/fixtures/repos.json b/main/fixtures/repos.json
index 3b79d964..f480000d 100644
--- a/main/fixtures/repos.json
+++ b/main/fixtures/repos.json
@@ -3,70 +3,84 @@
"pk": 5,
"model": "main.repo",
"fields": {
- "svn_root": "community",
- "testing": false,
+ "bugs_category": 33,
+ "staging": false,
"name": "Community",
- "bugs_project": 5
+ "bugs_project": 5,
+ "svn_root": "community",
+ "testing": false
}
},
{
"pk": 6,
"model": "main.repo",
"fields": {
- "svn_root": "community",
- "testing": true,
+ "bugs_category": 41,
+ "staging": false,
"name": "Community-Testing",
- "bugs_project": 5
+ "bugs_project": 5,
+ "svn_root": "community",
+ "testing": true
}
},
{
"pk": 1,
"model": "main.repo",
"fields": {
- "svn_root": "packages",
- "testing": false,
+ "bugs_category": 31,
+ "staging": false,
"name": "Core",
- "bugs_project": 1
+ "bugs_project": 1,
+ "svn_root": "packages",
+ "testing": false
}
},
{
"pk": 2,
"model": "main.repo",
"fields": {
- "svn_root": "packages",
- "testing": false,
+ "bugs_category": 2,
+ "staging": false,
"name": "Extra",
- "bugs_project": 1
+ "bugs_project": 1,
+ "svn_root": "packages",
+ "testing": false
}
},
{
"pk": 7,
"model": "main.repo",
"fields": {
- "svn_root": "community",
- "testing": false,
+ "bugs_category": 46,
+ "staging": false,
"name": "Multilib",
- "bugs_project": 5
+ "bugs_project": 5,
+ "svn_root": "community",
+ "testing": false
}
},
{
"pk": 8,
"model": "main.repo",
"fields": {
- "svn_root": "community",
- "testing": true,
+ "bugs_category": 46,
+ "staging": false,
"name": "Multilib-Testing",
- "bugs_project": 5
+ "bugs_project": 5,
+ "svn_root": "community",
+ "testing": true
}
},
{
"pk": 3,
"model": "main.repo",
"fields": {
- "svn_root": "packages",
- "testing": true,
+ "bugs_category": 10,
+ "staging": false,
"name": "Testing",
- "bugs_project": 1
+ "bugs_project": 1,
+ "svn_root": "packages",
+ "testing": true
}
},
{
diff --git a/main/migrations/0047_utc_datetimes.py b/main/migrations/0047_utc_datetimes.py
new file mode 100644
index 00000000..83153b78
--- /dev/null
+++ b/main/migrations/0047_utc_datetimes.py
@@ -0,0 +1,180 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+from django.utils.tzinfo import LocalTimezone
+
+def new_date(old_date, reverse=False):
+ if old_date is None:
+ return None
+ tz = LocalTimezone(old_date)
+ offset = tz.utcoffset(old_date)
+ if reverse:
+ offset = -offset
+ return old_date - offset
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ all_pkgs = orm.Package.objects.all()
+ for package in all_pkgs:
+ # prevents full object updates
+ orm.Package.objects.filter(pk=package.pk).update(
+ last_update=new_date(package.last_update),
+ files_last_update=new_date(package.files_last_update),
+ flag_date=new_date(package.flag_date))
+ # We could do todolists, but they just don't matter that much.
+
+ def backwards(self, orm):
+ all_pkgs = orm.Package.objects.all()
+ for package in all_pkgs:
+ # prevents full object updates
+ orm.Package.objects.filter(pk=package.pk).update(
+ last_update=new_date(package.last_update, True),
+ files_last_update=new_date(package.files_last_update, True),
+ flag_date=new_date(package.flag_date, True))
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'main.arch': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"},
+ 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+ },
+ 'main.donor': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Donor', 'db_table': "'donors'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
+ },
+ 'main.package': {
+ 'Meta': {'ordering': "('pkgname',)", 'object_name': 'Package', 'db_table': "'packages'"},
+ 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Arch']"}),
+ 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'compressed_size': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}),
+ 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'files_last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'flag_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'installed_size': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}),
+ 'last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkgdesc': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
+ 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Repo']"}),
+ 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'})
+ },
+ 'main.packagedepend': {
+ 'Meta': {'object_name': 'PackageDepend', 'db_table': "'package_depends'"},
+ 'depname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'depvcmp': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"})
+ },
+ 'main.packagefile': {
+ 'Meta': {'object_name': 'PackageFile', 'db_table': "'package_files'"},
+ 'directory': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_directory': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"})
+ },
+ 'main.repo': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"},
+ 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'staging': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'svn_root': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'testing': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'main.signoff': {
+ 'Meta': {'object_name': 'Signoff'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}),
+ 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'main.todolist': {
+ 'Meta': {'object_name': 'Todolist', 'db_table': "'todolists'"},
+ 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'date_added': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'description': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'main.todolistpkg': {
+ 'Meta': {'unique_together': "(('list', 'pkg'),)", 'object_name': 'TodolistPkg', 'db_table': "'todolist_pkgs'"},
+ 'complete': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'list': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Todolist']"}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"})
+ },
+ 'main.userprofile': {
+ 'Meta': {'object_name': 'UserProfile', 'db_table': "'user_profiles'"},
+ 'alias': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'allowed_repos': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Repo']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'favorite_distros': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'interests': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'languages': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'notify': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'occupation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'other_contact': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+ 'picture': ('django.db.models.fields.files.FileField', [], {'default': "'devs/silhouette.png'", 'max_length': '100'}),
+ 'public_email': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'roles': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'time_zone': ('django.db.models.fields.CharField', [], {'default': "'UTC'", 'max_length': '100'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'userprofile'", 'unique': 'True', 'to': "orm['auth.User']"}),
+ 'website': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+ 'yob': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['main']
diff --git a/main/migrations/0048_auto__add_field_repo_bugs_category.py b/main/migrations/0048_auto__add_field_repo_bugs_category.py
new file mode 100644
index 00000000..30575126
--- /dev/null
+++ b/main/migrations/0048_auto__add_field_repo_bugs_category.py
@@ -0,0 +1,158 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ db.add_column('repos', 'bugs_category', self.gf('django.db.models.fields.SmallIntegerField')(default=0), keep_default=False)
+
+ def backwards(self, orm):
+ db.delete_column('repos', 'bugs_category')
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'main.arch': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"},
+ 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+ },
+ 'main.donor': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Donor', 'db_table': "'donors'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
+ },
+ 'main.package': {
+ 'Meta': {'ordering': "('pkgname',)", 'object_name': 'Package', 'db_table': "'packages'"},
+ 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Arch']"}),
+ 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'compressed_size': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}),
+ 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'files_last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'flag_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'installed_size': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}),
+ 'last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkgdesc': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
+ 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Repo']"}),
+ 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'})
+ },
+ 'main.packagedepend': {
+ 'Meta': {'object_name': 'PackageDepend', 'db_table': "'package_depends'"},
+ 'depname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'depvcmp': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"})
+ },
+ 'main.packagefile': {
+ 'Meta': {'object_name': 'PackageFile', 'db_table': "'package_files'"},
+ 'directory': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_directory': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"})
+ },
+ 'main.repo': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"},
+ 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'staging': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'svn_root': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'testing': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'main.signoff': {
+ 'Meta': {'object_name': 'Signoff'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}),
+ 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'main.todolist': {
+ 'Meta': {'object_name': 'Todolist', 'db_table': "'todolists'"},
+ 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'date_added': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'description': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'main.todolistpkg': {
+ 'Meta': {'unique_together': "(('list', 'pkg'),)", 'object_name': 'TodolistPkg', 'db_table': "'todolist_pkgs'"},
+ 'complete': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'list': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Todolist']"}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"})
+ },
+ 'main.userprofile': {
+ 'Meta': {'object_name': 'UserProfile', 'db_table': "'user_profiles'"},
+ 'alias': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'allowed_repos': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Repo']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'favorite_distros': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'interests': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'languages': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'notify': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'occupation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'other_contact': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+ 'picture': ('django.db.models.fields.files.FileField', [], {'default': "'devs/silhouette.png'", 'max_length': '100'}),
+ 'public_email': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'roles': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'time_zone': ('django.db.models.fields.CharField', [], {'default': "'UTC'", 'max_length': '100'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'userprofile'", 'unique': 'True', 'to': "orm['auth.User']"}),
+ 'website': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+ 'yob': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['main']
diff --git a/main/models.py b/main/models.py
index 4370fa24..59dc154b 100644
--- a/main/models.py
+++ b/main/models.py
@@ -5,6 +5,7 @@ from django.contrib.sites.models import Site
from main.utils import cache_function, make_choice
from packages.models import PackageRelation
+from datetime import datetime
from itertools import groupby
import pytz
@@ -88,6 +89,8 @@ class Repo(models.Model):
help_text="Is this repo meant for package staging?")
bugs_project = models.SmallIntegerField(default=1,
help_text="Flyspray project ID for this repository.")
+ bugs_category = models.SmallIntegerField(default=0,
+ help_text="Flyspray category ID for this repository.")
svn_root = models.CharField(max_length=64,
help_text="SVN root (e.g. path) for this repository.")
@@ -103,8 +106,10 @@ class Repo(models.Model):
verbose_name_plural = 'repos'
class Package(models.Model):
- repo = models.ForeignKey(Repo, related_name="packages")
- arch = models.ForeignKey(Arch, related_name="packages")
+ repo = models.ForeignKey(Repo, related_name="packages",
+ on_delete=models.PROTECT)
+ arch = models.ForeignKey(Arch, related_name="packages",
+ on_delete=models.PROTECT)
pkgname = models.CharField(max_length=255, db_index=True)
pkgbase = models.CharField(max_length=255, db_index=True)
pkgver = models.CharField(max_length=255)
@@ -120,15 +125,15 @@ class Package(models.Model):
last_update = models.DateTimeField(null=True, blank=True)
files_last_update = models.DateTimeField(null=True, blank=True)
packager_str = models.CharField(max_length=255)
- packager = models.ForeignKey(User, null=True)
+ packager = models.ForeignKey(User, null=True,
+ on_delete=models.SET_NULL)
flag_date = models.DateTimeField(null=True)
objects = PackageManager()
class Meta:
db_table = 'packages'
ordering = ('pkgname',)
- #get_latest_by = 'last_update'
- #ordering = ('-last_update',)
+ get_latest_by = 'last_update'
def __unicode__(self):
return self.pkgname
@@ -183,9 +188,12 @@ class Package(models.Model):
"""
requiredby = PackageDepend.objects.select_related('pkg',
'pkg__arch', 'pkg__repo').filter(
- pkg__arch__in=self.applicable_arches(),
depname=self.pkgname).order_by(
- 'pkg__pkgname', 'pkg__id')
+ 'pkg__pkgname', 'pkg__arch__name', 'pkg__repo__name')
+ if not self.arch.agnostic:
+ # make sure we match architectures if possible
+ requiredby = requiredby.filter(
+ pkg__arch__in=self.applicable_arches())
# sort out duplicate packages; this happens if something has a double
# versioned dep such as a kernel module
requiredby = [list(vals)[0] for k, vals in
@@ -276,21 +284,6 @@ class Package(models.Model):
return Package.objects.filter(arch__in=self.applicable_arches(),
repo__testing=self.repo.testing, pkgbase=self.pkgbase).exclude(id=self.id)
- def get_svn_link(self, svnpath):
- linkbase = "http://projects.archlinux.org/svntogit/%s.git/tree/%s/%s/"
- return linkbase % (self.repo.svn_root, self.pkgbase, svnpath)
-
- def get_arch_svn_link(self):
- repo = self.repo.name.lower()
- return self.get_svn_link("repos/%s-%s" % (repo, self.arch.name))
-
- def get_trunk_svn_link(self):
- return self.get_svn_link("trunk")
-
- def get_bugs_link(self):
- return "https://bugs.archlinux.org/?project=%d&string=%s" % \
- (self.repo.bugs_project, self.pkgname)
-
def is_same_version(self, other):
'is this package similar, name and version-wise, to another'
return self.pkgname == other.pkgname \
@@ -348,10 +341,10 @@ class PackageDepend(models.Model):
db_table = 'package_depends'
class Todolist(models.Model):
- creator = models.ForeignKey(User)
+ creator = models.ForeignKey(User, on_delete=models.PROTECT)
name = models.CharField(max_length=255)
description = models.TextField()
- date_added = models.DateTimeField(auto_now_add=True, db_index=True)
+ date_added = models.DateTimeField(db_index=True)
objects = TodolistManager()
def __unicode__(self):
@@ -383,10 +376,18 @@ class TodolistPkg(models.Model):
db_table = 'todolist_pkgs'
unique_together = (('list','pkg'),)
+def set_todolist_fields(sender, **kwargs):
+ todolist = kwargs['instance']
+ if not todolist.date_added:
+ todolist.date_added = datetime.utcnow()
+
# connect signals needed to keep cache in line with reality
-from main.utils import refresh_package_latest
-from django.db.models.signals import post_save
-post_save.connect(refresh_package_latest, sender=Package,
+from main.utils import refresh_latest
+from django.db.models.signals import pre_save, post_save
+
+post_save.connect(refresh_latest, sender=Package,
+ dispatch_uid="main.models")
+pre_save.connect(set_todolist_fields, sender=Todolist,
dispatch_uid="main.models")
# vim: set ts=4 sw=4 et:
diff --git a/main/templatetags/attributes.py b/main/templatetags/attributes.py
new file mode 100644
index 00000000..bd4ccf3d
--- /dev/null
+++ b/main/templatetags/attributes.py
@@ -0,0 +1,21 @@
+import re
+from django import template
+from django.conf import settings
+
+numeric_test = re.compile("^\d+$")
+register = template.Library()
+
+def attribute(value, arg):
+ """Gets an attribute of an object dynamically from a string name"""
+ if hasattr(value, str(arg)):
+ return getattr(value, arg)
+ elif hasattr(value, 'has_key') and value.has_key(arg):
+ return value[arg]
+ elif numeric_test.match(str(arg)) and len(value) > int(arg):
+ return value[int(arg)]
+ else:
+ return settings.TEMPLATE_STRING_IF_INVALID
+
+register.filter('attribute', attribute)
+
+# vim: set ts=4 sw=4 et:
diff --git a/main/utils.py b/main/utils.py
index d7681cb6..12d12503 100644
--- a/main/utils.py
+++ b/main/utils.py
@@ -6,10 +6,8 @@ from django.core.cache import cache
from django.utils.hashcompat import md5_constructor
CACHE_TIMEOUT = 1800
-INVALIDATE_TIMEOUT = 15
-
-CACHE_PACKAGE_KEY = 'cache_package_latest'
-CACHE_NEWS_KEY = 'cache_news_latest'
+INVALIDATE_TIMEOUT = 10
+CACHE_LATEST_PREFIX = 'cache_latest_'
def cache_function_key(func, args, kwargs):
raw = [func.__name__, func.__module__, args, kwargs]
@@ -53,16 +51,34 @@ make_choice = lambda l: [(str(m), str(m)) for m in l]
# and hoops otherwise. The only thing currently using these keys is the feed
# caching stuff.
-def refresh_package_latest(**kwargs):
+def refresh_latest(**kwargs):
+ '''A post_save signal handler to clear out the cached latest value for a
+ given model.'''
+ cache_key = CACHE_LATEST_PREFIX + kwargs['sender'].__name__
# We could delete the value, but that could open a race condition
# where the new data wouldn't have been committed yet by the calling
# thread. Instead, explicitly set it to None for a short amount of time.
# Hopefully by the time it expires we will have committed, and the cache
# will be valid again. See "Scaling Django" by Mike Malone, slide 30.
- cache.set(CACHE_PACKAGE_KEY, None, INVALIDATE_TIMEOUT)
+ cache.set(cache_key, None, INVALIDATE_TIMEOUT)
-def refresh_news_latest(**kwargs):
- # same thoughts apply as in refresh_package_latest
- cache.set(CACHE_NEWS_KEY, None, INVALIDATE_TIMEOUT)
+def retrieve_latest(sender):
+ # we could break this down based on the request url, but it would probably
+ # cost us more in query time to do so.
+ cache_key = CACHE_LATEST_PREFIX + sender.__name__
+ latest = cache.get(cache_key)
+ if latest:
+ return latest
+ try:
+ latest_by = sender._meta.get_latest_by
+ latest = sender.objects.values(latest_by).latest()[latest_by]
+ # Using add means "don't overwrite anything in there". What could be in
+ # there is an explicit None value that our refresh signal set, which
+ # means we want to avoid race condition possibilities for a bit.
+ cache.add(cache_key, latest, CACHE_TIMEOUT)
+ return latest
+ except sender.DoesNotExist:
+ pass
+ return None
# vim: set ts=4 sw=4 et:
diff --git a/media/archweb.css b/media/archweb.css
index f1edebe6..ab88c86f 100644
--- a/media/archweb.css
+++ b/media/archweb.css
@@ -260,6 +260,11 @@ ul.admin-actions li { display: inline; padding-left: 1.5em; }
#dev-signoffs .signoff-no { color: red; }
#dev-signoffs .signed-username { color: #888; margin-left: 0.5em; }
+/* iso testing feedback form */
+#releng-feedback label { width: auto; display: inline; font-weight: normal; }
+#releng-feedback ul { padding-left: 1em; }
+#releng-feedback li { list-style: none; }
+
/* highlight current website in the navbar */
#archnavbar.anb-home ul li#anb-home a { color: white !important; }
#archnavbar.anb-packages ul li#anb-packages a { color: white !important; }
diff --git a/media/archweb.js b/media/archweb.js
index 03358fa9..49f2a319 100644
--- a/media/archweb.js
+++ b/media/archweb.js
@@ -67,6 +67,36 @@ if (typeof $.tablesorter !== 'undefined') {
},
type: 'numeric'
});
+ $.tablesorter.addParser({
+ id: 'filesize',
+ re: /^(\d+(?:\.\d+)?) (bytes?|KB|MB|GB|TB|PB)$/,
+ is: function(s) {
+ return this.re.test(s);
+ },
+ format: function(s) {
+ var matches = this.re.exec(s);
+ if (!matches) return 0;
+ var size = parseFloat(matches[1]);
+ var suffix = matches[2];
+
+ switch(suffix) {
+ case 'byte':
+ case 'bytes':
+ return size;
+ case 'KB':
+ return size * 1024;
+ case 'MB':
+ return size * 1024 * 1024;
+ case 'GB':
+ return size * 1024 * 1024 * 1024;
+ case 'TB':
+ return size * 1024 * 1024 * 1024 * 1024;
+ case 'PB':
+ return size * 1024 * 1024 * 1024 * 1024 * 1024;
+ }
+ },
+ type: 'numeric'
+ });
}
/* news/add.html */
diff --git a/mirrors/admin.py b/mirrors/admin.py
index b9c2876a..b7b478de 100644
--- a/mirrors/admin.py
+++ b/mirrors/admin.py
@@ -60,7 +60,7 @@ class MirrorAdminForm(forms.ModelForm):
class MirrorAdmin(admin.ModelAdmin):
form = MirrorAdminForm
list_display = ('name', 'tier', 'country', 'active', 'public', 'isos', 'admin_email', 'supported_protocols')
- list_filter = ('tier', 'country', 'active', 'public')
+ list_filter = ('tier', 'active', 'public', 'country')
search_fields = ('name',)
inlines = [
MirrorUrlInlineAdmin,
diff --git a/mirrors/management/commands/mirrorcheck.py b/mirrors/management/commands/mirrorcheck.py
index 51be71ea..ea43d558 100644
--- a/mirrors/management/commands/mirrorcheck.py
+++ b/mirrors/management/commands/mirrorcheck.py
@@ -13,7 +13,7 @@ from django.core.management.base import NoArgsCommand
from django.db import transaction
from collections import deque
-from datetime import datetime, timedelta
+from datetime import datetime
import logging
import re
import socket
@@ -52,22 +52,6 @@ class Command(NoArgsCommand):
return check_current_mirrors()
-def parse_rfc3339_datetime(time_string):
- # '2010-09-02 11:05:06+02:00'
- m = re.match('^(\d{4})-(\d{2})-(\d{2}) '
- '(\d{2}):(\d{2}):(\d{2})([-+])(\d{2}):(\d{2})', time_string)
- if m:
- vals = m.groups()
- parsed = datetime(int(vals[0]), int(vals[1]), int(vals[2]),
- int(vals[3]), int(vals[4]), int(vals[5]))
- # now account for time zone offset
- sign = vals[6]
- offset = timedelta(hours=int(sign + vals[7]),
- minutes=int(sign + vals[8]))
- # subtract the offset, e.g. '-04:00' should be moved up 4 hours
- return parsed - offset
- return None
-
def check_mirror_url(mirror_url):
url = mirror_url.url + 'lastsync'
logger.info("checking URL %s", url)
@@ -78,18 +62,14 @@ def check_mirror_url(mirror_url):
data = result.read()
result.close()
end = time.time()
- # lastsync should be an epoch value, but some mirrors
- # are creating their own in RFC-3339 format:
- # '2010-09-02 11:05:06+02:00'
+ # lastsync should be an epoch value created by us
parsed_time = None
try:
parsed_time = datetime.utcfromtimestamp(int(data))
except ValueError:
# it is bad news to try logging the lastsync value;
# sometimes we get a crazy-encoded web page.
- logger.info("attempting to parse generated lastsync file"
- " from mirror %s", url)
- parsed_time = parse_rfc3339_datetime(data)
+ pass
log.last_sync = parsed_time
# if we couldn't parse a time, this is a failure
@@ -154,8 +134,8 @@ class MirrorCheckPool(object):
@transaction.commit_on_success
def run(self):
logger.debug("starting threads")
- for t in self.threads:
- t.start()
+ for thread in self.threads:
+ thread.start()
logger.debug("joining on all threads")
self.tasks.join()
logger.debug("processing log entries")
diff --git a/mirrors/management/commands/mirrorresolv.py b/mirrors/management/commands/mirrorresolv.py
index 8a628bd4..4e812f2d 100644
--- a/mirrors/management/commands/mirrorresolv.py
+++ b/mirrors/management/commands/mirrorresolv.py
@@ -49,6 +49,6 @@ def resolve_mirrors():
mirrorurl.has_ipv4, mirrorurl.has_ipv6)
mirrorurl.save(force_update=True)
except socket.error, e:
- logger.warn("error resolving %s: %s", hostname, e)
+ logger.warn("error resolving %s: %s", mirrorurl.hostname, e)
# vim: set ts=4 sw=4 et:
diff --git a/mirrors/migrations/0007_unique_names_urls.py b/mirrors/migrations/0007_unique_names_urls.py
new file mode 100644
index 00000000..49c0fbb7
--- /dev/null
+++ b/mirrors/migrations/0007_unique_names_urls.py
@@ -0,0 +1,66 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ db.create_unique('mirrors_mirror', ['name'])
+ db.create_unique('mirrors_mirrorurl', ['url'])
+
+ def backwards(self, orm):
+ db.delete_unique('mirrors_mirrorurl', ['url'])
+ db.delete_unique('mirrors_mirror', ['name'])
+
+ models = {
+ 'mirrors.mirror': {
+ 'Meta': {'ordering': "('country', 'name')", 'object_name': 'Mirror'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'admin_email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'blank': 'True'}),
+ 'country': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'isos': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'notes': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'rsync_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}),
+ 'rsync_user': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}),
+ 'tier': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}),
+ 'upstream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['mirrors.Mirror']", 'null': 'True'})
+ },
+ 'mirrors.mirrorlog': {
+ 'Meta': {'object_name': 'MirrorLog'},
+ 'check_time': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'duration': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+ 'error': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'url': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['mirrors.MirrorUrl']"})
+ },
+ 'mirrors.mirrorprotocol': {
+ 'Meta': {'ordering': "('protocol',)", 'object_name': 'MirrorProtocol'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_download': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'protocol': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '10'})
+ },
+ 'mirrors.mirrorrsync': {
+ 'Meta': {'object_name': 'MirrorRsync'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.CharField', [], {'max_length': '24'}),
+ 'mirror': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'rsync_ips'", 'to': "orm['mirrors.Mirror']"})
+ },
+ 'mirrors.mirrorurl': {
+ 'Meta': {'object_name': 'MirrorUrl'},
+ 'has_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'has_ipv6': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mirror': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'urls'", 'to': "orm['mirrors.Mirror']"}),
+ 'protocol': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'urls'", 'to': "orm['mirrors.MirrorProtocol']"}),
+ 'url': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['mirrors']
diff --git a/mirrors/migrations/0008_auto__add_field_mirrorurl_country.py b/mirrors/migrations/0008_auto__add_field_mirrorurl_country.py
new file mode 100644
index 00000000..660ac080
--- /dev/null
+++ b/mirrors/migrations/0008_auto__add_field_mirrorurl_country.py
@@ -0,0 +1,67 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding field 'MirrorUrl.country'
+ db.add_column('mirrors_mirrorurl', 'country', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=255, null=True, blank=True), keep_default=False)
+
+ def backwards(self, orm):
+ # Deleting field 'MirrorUrl.country'
+ db.delete_column('mirrors_mirrorurl', 'country')
+
+ models = {
+ 'mirrors.mirror': {
+ 'Meta': {'ordering': "('country', 'name')", 'object_name': 'Mirror'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'admin_email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'blank': 'True'}),
+ 'country': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'isos': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'notes': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'rsync_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}),
+ 'rsync_user': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}),
+ 'tier': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}),
+ 'upstream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['mirrors.Mirror']", 'null': 'True'})
+ },
+ 'mirrors.mirrorlog': {
+ 'Meta': {'object_name': 'MirrorLog'},
+ 'check_time': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'duration': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+ 'error': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'url': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['mirrors.MirrorUrl']"})
+ },
+ 'mirrors.mirrorprotocol': {
+ 'Meta': {'ordering': "('protocol',)", 'object_name': 'MirrorProtocol'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_download': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'protocol': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '10'})
+ },
+ 'mirrors.mirrorrsync': {
+ 'Meta': {'object_name': 'MirrorRsync'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.CharField', [], {'max_length': '24'}),
+ 'mirror': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'rsync_ips'", 'to': "orm['mirrors.Mirror']"})
+ },
+ 'mirrors.mirrorurl': {
+ 'Meta': {'object_name': 'MirrorUrl'},
+ 'country': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'has_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'has_ipv6': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mirror': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'urls'", 'to': "orm['mirrors.Mirror']"}),
+ 'protocol': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'urls'", 'to': "orm['mirrors.MirrorProtocol']"}),
+ 'url': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['mirrors']
diff --git a/mirrors/models.py b/mirrors/models.py
index 401821a8..bcde210c 100644
--- a/mirrors/models.py
+++ b/mirrors/models.py
@@ -12,9 +12,9 @@ TIER_CHOICES = (
)
class Mirror(models.Model):
- name = models.CharField(max_length=255)
+ name = models.CharField(max_length=255, unique=True)
tier = models.SmallIntegerField(default=2, choices=TIER_CHOICES)
- upstream = models.ForeignKey('self', null=True)
+ upstream = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
country = models.CharField(max_length=255, db_index=True)
admin_email = models.EmailField(max_length=255, blank=True)
public = models.BooleanField(default=True)
@@ -54,10 +54,12 @@ class MirrorProtocol(models.Model):
ordering = ('protocol',)
class MirrorUrl(models.Model):
- url = models.CharField(max_length=255)
+ url = models.CharField(max_length=255, unique=True)
protocol = models.ForeignKey(MirrorProtocol, related_name="urls",
- editable=False)
+ editable=False, on_delete=models.PROTECT)
mirror = models.ForeignKey(Mirror, related_name="urls")
+ country = models.CharField(max_length=255, blank=True, null=True,
+ db_index=True)
has_ipv4 = models.BooleanField("IPv4 capable", default=True,
editable=False)
has_ipv6 = models.BooleanField("IPv6 capable", default=False,
@@ -73,6 +75,10 @@ class MirrorUrl(models.Model):
def hostname(self):
return urlparse(self.url).hostname
+ @property
+ def real_country(self):
+ return self.country or self.mirror.country
+
def clean(self):
try:
# Auto-map the protocol field by looking at the URL
diff --git a/mirrors/utils.py b/mirrors/utils.py
index 124b66e6..686ec581 100644
--- a/mirrors/utils.py
+++ b/mirrors/utils.py
@@ -7,6 +7,25 @@ import datetime
default_cutoff = datetime.timedelta(hours=24)
+def annotate_url(url, delays):
+ '''Given a MirrorURL object, add a few more attributes to it regarding
+ status, including completion_pct, delay, and score.'''
+ url.completion_pct = float(url.success_count) / url.check_count
+ if url.id in delays:
+ url_delays = delays[url.id]
+ url.delay = sum(url_delays, datetime.timedelta()) / len(url_delays)
+ hours = url.delay.days * 24.0 + url.delay.seconds / 3600.0
+
+ if url.completion_pct > 0:
+ divisor = url.completion_pct
+ else:
+ # arbitrary small value
+ divisor = 0.005
+ url.score = (hours + url.duration_avg + url.duration_stddev) / divisor
+ else:
+ url.delay = None
+ url.score = None
+
@cache_function(300)
def get_mirror_statuses(cutoff=default_cutoff):
cutoff_time = datetime.datetime.utcnow() - cutoff
@@ -31,8 +50,8 @@ def get_mirror_statuses(cutoff=default_cutoff):
check_time__gte=cutoff_time)
delays = {}
for log in times:
- d = log.check_time - log.last_sync
- delays.setdefault(log.url_id, []).append(d)
+ delay = log.check_time - log.last_sync
+ delays.setdefault(log.url_id, []).append(delay)
if urls:
last_check = max([u.last_check for u in urls])
@@ -44,29 +63,14 @@ def get_mirror_statuses(cutoff=default_cutoff):
check_frequency = (check_info['mx'] - check_info['mn']) \
/ (num_checks - 1)
else:
- check_frequency = None;
+ check_frequency = None
else:
last_check = None
num_checks = 0
check_frequency = None
for url in urls:
- url.completion_pct = float(url.success_count) / url.check_count
- if url.id in delays:
- url_delays = delays[url.id]
- d = sum(url_delays, datetime.timedelta()) / len(url_delays)
- url.delay = d
- hours = d.days * 24.0 + d.seconds / 3600.0
-
- if url.completion_pct > 0:
- divisor = url.completion_pct
- else:
- # arbitrary small value
- divisor = 0.005
- url.score = (hours + url.duration_avg + url.duration_stddev) / divisor
- else:
- url.delay = None
- url.score = None
+ annotate_url(url, delays)
return {
'cutoff': cutoff,
@@ -82,10 +86,13 @@ def get_mirror_errors(cutoff=default_cutoff):
errors = MirrorLog.objects.filter(
is_success=False, check_time__gte=cutoff_time,
url__mirror__active=True, url__mirror__public=True).values(
- 'url__url', 'url__protocol__protocol', 'url__mirror__country',
- 'error').annotate(
+ 'url__url', 'url__country', 'url__protocol__protocol',
+ 'url__mirror__country', 'error').annotate(
error_count=Count('error'), last_occurred=Max('check_time')
).order_by('-last_occurred', '-error_count')
- return list(errors)
+ errors = list(errors)
+ for err in errors:
+ err['country'] = err['url__country'] or err['url__mirror__country']
+ return errors
# vim: set ts=4 sw=4 et:
diff --git a/mirrors/views.py b/mirrors/views.py
index a2b94de8..f03a2e8a 100644
--- a/mirrors/views.py
+++ b/mirrors/views.py
@@ -1,6 +1,5 @@
from django import forms
from django.core.serializers.json import DjangoJSONEncoder
-from django.db.models import Avg, Count, Max, Min, StdDev
from django.db.models import Q
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
@@ -23,10 +22,10 @@ class MirrorlistForm(forms.Form):
def __init__(self, *args, **kwargs):
super(MirrorlistForm, self).__init__(*args, **kwargs)
- mirrors = Mirror.objects.filter(active=True).values_list(
+ countries = Mirror.objects.filter(active=True).values_list(
'country', flat=True).distinct().order_by('country')
self.fields['country'].choices = [('all','All')] + make_choice(
- mirrors)
+ countries)
self.fields['country'].initial = ['all']
protos = make_choice(
MirrorProtocol.objects.filter(is_download=True))
@@ -61,7 +60,8 @@ def find_mirrors(request, countries=None, protocols=None, use_status=False,
mirror__public=True, mirror__active=True, mirror__isos=True
)
if countries and 'all' not in countries:
- qset = qset.filter(mirror__country__in=countries)
+ qset = qset.filter(Q(country__in=countries) |
+ Q(mirror__country__in=countries))
ip_version = Q()
if ipv4_supported:
@@ -71,7 +71,8 @@ def find_mirrors(request, countries=None, protocols=None, use_status=False,
qset = qset.filter(ip_version)
if not use_status:
- urls = qset.order_by('mirror__country', 'mirror__name', 'url')
+ urls = qset.order_by('mirror__name', 'url')
+ urls = sorted(urls, key=lambda x: x.real_country)
template = 'mirrors/mirrorlist.txt'
else:
status_info = get_mirror_statuses()
@@ -94,20 +95,29 @@ def find_mirrors(request, countries=None, protocols=None, use_status=False,
mimetype='text/plain')
def mirrors(request):
- mirrors = Mirror.objects.select_related().order_by('tier', 'country')
+ mirror_list = Mirror.objects.select_related().order_by('tier', 'country')
if not request.user.is_authenticated():
- mirrors = mirrors.filter(public=True, active=True)
+ mirror_list = mirror_list.filter(public=True, active=True)
return direct_to_template(request, 'mirrors/mirrors.html',
- {'mirror_list': mirrors})
+ {'mirror_list': mirror_list})
def mirror_details(request, name):
mirror = get_object_or_404(Mirror, name=name)
if not request.user.is_authenticated() and \
(not mirror.public or not mirror.active):
- # TODO: maybe this should be 403? but that would leak existence
raise Http404
+
+ status_info = get_mirror_statuses()
+ checked_urls = [url for url in status_info['urls'] \
+ if url.mirror_id == mirror.id]
+ all_urls = mirror.urls.select_related('protocol')
+ # get each item from checked_urls and supplement with anything in all_urls
+ # if it wasn't there
+ all_urls = set(checked_urls).union(all_urls)
+ all_urls = sorted(all_urls, key=lambda x: x.url)
+
return direct_to_template(request, 'mirrors/mirror_details.html',
- {'mirror': mirror})
+ {'mirror': mirror, 'urls': all_urls})
def status(request):
bad_timedelta = datetime.timedelta(days=3)
@@ -149,7 +159,7 @@ class MirrorStatusJSONEncoder(DjangoJSONEncoder):
for attr in self.url_attributes:
data[attr] = getattr(obj, attr)
# separate because it isn't on the URL directly
- data['country'] = obj.mirror.country
+ data['country'] = obj.real_country
return data
if isinstance(obj, MirrorProtocol):
return unicode(obj)
diff --git a/news/migrations/0007_add_guid.py b/news/migrations/0007_add_guid.py
new file mode 100644
index 00000000..5fa8193e
--- /dev/null
+++ b/news/migrations/0007_add_guid.py
@@ -0,0 +1,65 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ db.add_column('news', 'guid', self.gf('django.db.models.fields.CharField')(default='', max_length=255), keep_default=False)
+
+ def backwards(self, orm):
+ db.delete_column('news', 'guid')
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'news.news': {
+ 'Meta': {'ordering': "['-postdate']", 'object_name': 'News', 'db_table': "'news'"},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'news_author'", 'to': "orm['auth.User']"}),
+ 'content': ('django.db.models.fields.TextField', [], {}),
+ 'guid': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'postdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['news']
diff --git a/news/migrations/0008_set_prior_guids.py b/news/migrations/0008_set_prior_guids.py
new file mode 100644
index 00000000..704b11c9
--- /dev/null
+++ b/news/migrations/0008_set_prior_guids.py
@@ -0,0 +1,83 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.conf import settings
+from django.db import models
+
+class Migration(DataMigration):
+ '''The point of this migration is to not mark every news item as 'new' in
+ people's feed readers, and store the GUID perminantly with the news item.
+ All previously published news items will get their former auto-assigned
+ GUID; new ones will get a generated tag: URI and this won't apply to
+ them.'''
+
+ def forwards(self, orm):
+ all_news = orm.News.objects.all().defer('content')
+ site = orm['sites.site'].objects.get(pk=settings.SITE_ID).domain
+ for news in all_news:
+ new_guid = 'http://%s/news/%s/' % (site, news.slug)
+ # looks totally silly, but prevents full updates of all fields,
+ # including content and last_modified which we want to leave alone
+ orm.News.objects.filter(pk=news.pk).update(guid=new_guid)
+
+ def backwards(self, orm):
+ pass
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'news.news': {
+ 'Meta': {'ordering': "['-postdate']", 'object_name': 'News', 'db_table': "'news'"},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'news_author'", 'to': "orm['auth.User']"}),
+ 'content': ('django.db.models.fields.TextField', [], {}),
+ 'guid': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'postdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'sites.site': {
+ 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"},
+ 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ }
+ }
+
+ complete_apps = ['sites', 'news']
diff --git a/news/migrations/0009_utc_datetimes.py b/news/migrations/0009_utc_datetimes.py
new file mode 100644
index 00000000..6cddf783
--- /dev/null
+++ b/news/migrations/0009_utc_datetimes.py
@@ -0,0 +1,85 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+from django.utils.tzinfo import LocalTimezone
+
+def new_date(old_date, reverse=False):
+ if old_date is None:
+ return None
+ tz = LocalTimezone(old_date)
+ offset = tz.utcoffset(old_date)
+ if reverse:
+ offset = -offset
+ return old_date - offset
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ all_news = orm.News.objects.all().defer('content')
+ for news in all_news:
+ # prevents full object updates
+ orm.News.objects.filter(pk=news.pk).update(
+ postdate=new_date(news.postdate),
+ last_modified=new_date(news.last_modified))
+
+ def backwards(self, orm):
+ all_news = orm.News.objects.all().defer('content')
+ for news in all_news:
+ # prevents full object updates
+ orm.News.objects.filter(pk=news.pk).update(
+ postdate=new_date(news.postdate, True),
+ last_modified=new_date(news.last_modified, True))
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'news.news': {
+ 'Meta': {'ordering': "['-postdate']", 'object_name': 'News', 'db_table': "'news'"},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'news_author'", 'to': "orm['auth.User']"}),
+ 'content': ('django.db.models.fields.TextField', [], {}),
+ 'guid': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'postdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['news']
diff --git a/news/models.py b/news/models.py
index c2d644b7..33d958e0 100644
--- a/news/models.py
+++ b/news/models.py
@@ -1,13 +1,17 @@
+from datetime import datetime
+
from django.db import models
from django.contrib.auth.models import User
+from django.contrib.sites.models import Site
class News(models.Model):
slug = models.SlugField(max_length=255, unique=True)
- author = models.ForeignKey(User, related_name='news_author')
- postdate = models.DateTimeField("post date", auto_now_add=True, db_index=True)
- last_modified = models.DateTimeField(editable=False,
- auto_now=True, db_index=True)
+ author = models.ForeignKey(User, related_name='news_author',
+ on_delete=models.PROTECT)
+ postdate = models.DateTimeField("post date", db_index=True)
+ last_modified = models.DateTimeField(editable=False, db_index=True)
title = models.CharField(max_length=255)
+ guid = models.CharField(max_length=255, editable=False)
content = models.TextField()
def get_absolute_url(self):
@@ -22,10 +26,23 @@ class News(models.Model):
get_latest_by = 'postdate'
ordering = ['-postdate']
+def set_news_fields(sender, **kwargs):
+ news = kwargs['instance']
+ now = datetime.utcnow()
+ news.last_modified = now
+ if not news.postdate:
+ news.postdate = now
+ # http://diveintomark.org/archives/2004/05/28/howto-atom-id
+ news.guid = 'tag:%s,%s:%s' % (Site.objects.get_current(),
+ now.strftime('%Y-%m-%d'), news.get_absolute_url())
+
# connect signals needed to keep cache in line with reality
-from main.utils import refresh_news_latest
-from django.db.models.signals import post_save
-post_save.connect(refresh_news_latest, sender=News,
+from main.utils import refresh_latest
+from django.db.models.signals import pre_save, post_save
+
+post_save.connect(refresh_latest, sender=News,
+ dispatch_uid="news.models")
+pre_save.connect(set_news_fields, sender=News,
dispatch_uid="news.models")
# vim: set ts=4 sw=4 et:
diff --git a/packages/migrations/0007_auto__add_field_packagerelation_created.py b/packages/migrations/0007_auto__add_field_packagerelation_created.py
new file mode 100644
index 00000000..37321fdb
--- /dev/null
+++ b/packages/migrations/0007_auto__add_field_packagerelation_created.py
@@ -0,0 +1,135 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ db.add_column('packages_packagerelation', 'created', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow()), keep_default=False)
+
+ def backwards(self, orm):
+ db.delete_column('packages_packagerelation', 'created')
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'main.arch': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"},
+ 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+ },
+ 'main.package': {
+ 'Meta': {'ordering': "('pkgname',)", 'object_name': 'Package', 'db_table': "'packages'"},
+ 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Arch']"}),
+ 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'compressed_size': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}),
+ 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'files_last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'flag_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'installed_size': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}),
+ 'last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkgdesc': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
+ 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Repo']"}),
+ 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'})
+ },
+ 'main.repo': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"},
+ 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'staging': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'svn_root': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'testing': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'packages.conflict': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Conflict'},
+ 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'conflicts'", 'to': "orm['main.Package']"}),
+ 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'})
+ },
+ 'packages.license': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'License'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'licenses'", 'to': "orm['main.Package']"})
+ },
+ 'packages.packagegroup': {
+ 'Meta': {'object_name': 'PackageGroup'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Package']"})
+ },
+ 'packages.packagerelation': {
+ 'Meta': {'unique_together': "(('pkgbase', 'user', 'type'),)", 'object_name': 'PackageRelation'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'type': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_relations'", 'to': "orm['auth.User']"})
+ },
+ 'packages.provision': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Provision'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'provides'", 'to': "orm['main.Package']"}),
+ 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'})
+ },
+ 'packages.replacement': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Replacement'},
+ 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'replaces'", 'to': "orm['main.Package']"}),
+ 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['packages']
diff --git a/packages/models.py b/packages/models.py
index 79e8abca..a950bddb 100644
--- a/packages/models.py
+++ b/packages/models.py
@@ -1,5 +1,7 @@
+from datetime import datetime
+
from django.db import models
-from django.db.models.signals import post_save
+from django.db.models.signals import pre_save, post_save
from django.contrib.auth.models import User
class PackageRelation(models.Model):
@@ -18,6 +20,7 @@ class PackageRelation(models.Model):
pkgbase = models.CharField(max_length=255)
user = models.ForeignKey(User, related_name="package_relations")
type = models.PositiveIntegerField(choices=TYPE_CHOICES, default=MAINTAINER)
+ created = models.DateTimeField(editable=False)
def get_associated_packages(self):
# TODO: delayed import to avoid circular reference
@@ -109,7 +112,15 @@ def remove_inactive_maintainers(sender, instance, created, **kwargs):
type=PackageRelation.MAINTAINER)
maint_relations.delete()
+def set_created_field(sender, **kwargs):
+ # We use this same callback for both Isos and Tests
+ obj = kwargs['instance']
+ if not obj.created:
+ obj.created = datetime.utcnow()
+
post_save.connect(remove_inactive_maintainers, sender=User,
dispatch_uid="packages.models")
+pre_save.connect(set_created_field, sender=PackageRelation,
+ dispatch_uid="packages.models")
# vim: set ts=4 sw=4 et:
diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py
index dd5b9347..e089b723 100644
--- a/packages/templatetags/package_extras.py
+++ b/packages/templatetags/package_extras.py
@@ -1,4 +1,4 @@
-import urllib
+from urllib import urlencode, quote as urlquote
try:
from urlparse import parse_qs
except ImportError:
@@ -22,7 +22,7 @@ class BuildQueryStringNode(template.Node):
qs['sort'] = ['-' + self.sortfield]
else:
qs['sort'] = [self.sortfield]
- return urllib.urlencode(qs, True)
+ return urlencode(qs, True)
@register.tag(name='buildsortqs')
def do_buildsortqs(parser, token):
@@ -48,4 +48,37 @@ def userpkgs(user):
)
return ''
+
+def svn_link(package, svnpath):
+ '''Helper function for the two real SVN link methods.'''
+ parts = (package.repo.svn_root, package.pkgbase, svnpath)
+ linkbase = "http://projects.archlinux.org/svntogit/%s.git/tree/%s/%s/"
+ return linkbase % tuple(urlquote(part) for part in parts)
+
+@register.simple_tag
+def svn_arch(package):
+ repo = package.repo.name.lower()
+ return svn_link(package, "repos/%s-%s" % (repo, package.arch.name))
+
+@register.simple_tag
+def svn_trunk(package):
+ return svn_link(package, "trunk")
+
+@register.simple_tag
+def bugs_list(package):
+ data = {
+ 'project': package.repo.bugs_project,
+ 'string': package.pkgname,
+ }
+ return "https://bugs.archlinux.org/?%s" % urlencode(data)
+
+@register.simple_tag
+def bug_report(package):
+ data = {
+ 'project': package.repo.bugs_project,
+ 'product_category': package.repo.bugs_category,
+ 'item_summary': '[%s]' % package.pkgname,
+ }
+ return "https://bugs.archlinux.org/newtask?%s" % urlencode(data)
+
# vim: set ts=4 sw=4 et:
diff --git a/packages/urls.py b/packages/urls.py
index 638a370a..e0362fa2 100644
--- a/packages/urls.py
+++ b/packages/urls.py
@@ -5,6 +5,7 @@ package_patterns = patterns('packages.views',
(r'^files/$', 'files'),
(r'^maintainer/$', 'getmaintainer'),
(r'^flag/$', 'flag'),
+ (r'^flag/done/$', 'flag_confirmed', {}, 'package-flag-confirmed'),
(r'^unflag/$', 'unflag'),
(r'^unflag/all/$', 'unflag_all'),
(r'^download/$', 'download'),
@@ -17,10 +18,6 @@ urlpatterns = patterns('packages.views',
'signoff_package'),
(r'^update/$', 'update'),
- # Preference is for the non-search url below, but search is kept
- # because other projects link to it
- (r'^search/$', 'search'),
- (r'^search/(?P<page>\d+)/$', 'search'),
(r'^$', 'search'),
(r'^(?P<page>\d+)/$', 'search'),
diff --git a/packages/views.py b/packages/views.py
index 73594507..adf6c0af 100644
--- a/packages/views.py
+++ b/packages/views.py
@@ -18,6 +18,7 @@ from django.views.generic.simple import direct_to_template
from datetime import datetime
import string
+from urllib import urlencode
from main.models import Package, PackageFile
from main.models import Arch, Repo, Signoff
@@ -84,7 +85,8 @@ def update(request):
def details(request, name='', repo='', arch=''):
if all([name, repo, arch]):
try:
- pkg = Package.objects.get(pkgname=name,
+ 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, })
@@ -107,8 +109,14 @@ def details(request, name='', repo='', arch=''):
return direct_to_template(request, 'packages/packages_list.html',
context)
else:
- return redirect("/packages/?arch=%s&repo=%s&q=%s" % (
- arch.lower(), repo.title(), name))
+ pkg_data = [
+ ('arch', arch.lower()),
+ ('repo', repo.lower()),
+ ('q', name),
+ ]
+ # only include non-blank values in the query we generate
+ pkg_data = [(x, y) for x, y in pkg_data if y]
+ return redirect("/packages/?%s" % urlencode(pkg_data))
def groups(request, arch=None):
arches = []
@@ -163,7 +171,7 @@ class LimitTypedChoiceField(forms.TypedChoiceField):
try:
coerce_limit_value(value)
return True
- except ValueError, TypeError:
+ except (ValueError, TypeError):
return False
class PackageSearchForm(forms.Form):
@@ -218,7 +226,7 @@ def search(request, page=None):
packages = packages.filter(pkgbase__in=inner_q)
if form.cleaned_data['flagged'] == 'Flagged':
- packages=packages.filter(flag_date__isnull=False)
+ packages = packages.filter(flag_date__isnull=False)
elif form.cleaned_data['flagged'] == 'Not Flagged':
packages = packages.filter(flag_date__isnull=True)
@@ -359,18 +367,21 @@ class FlagForm(forms.Form):
def flag(request, name, repo, arch):
pkg = get_object_or_404(Package,
pkgname=name, repo__name__iexact=repo, arch__name=arch)
- context = {'pkg': pkg}
if pkg.flag_date is not None:
# already flagged. do nothing.
- return direct_to_template(request, 'packages/flagged.html', context)
+ return direct_to_template(request, 'packages/flagged.html', {'pkg': pkg})
+ # find all packages from (hopefully) the same PKGBUILD
+ pkgs = Package.objects.select_related('arch', 'repo').filter(
+ pkgbase=pkg.pkgbase, flag_date__isnull=True,
+ repo__testing=pkg.repo.testing).order_by(
+ 'pkgname', 'repo__name', 'arch__name')
if request.POST:
form = FlagForm(request.POST)
if form.is_valid() and form.cleaned_data['website'] == '':
- # find all packages from (hopefully) the same PKGBUILD
- pkgs = Package.objects.filter(
- pkgbase=pkg.pkgbase, repo__testing=pkg.repo.testing)
- pkgs.update(flag_date=datetime.now())
+ # save the package list for later use
+ flagged_pkgs = list(pkgs)
+ pkgs.update(flag_date=datetime.utcnow())
maints = pkg.maintainers
if not maints:
@@ -386,13 +397,13 @@ def flag(request, name, repo, arch):
toemail.append(maint.email)
if toemail:
- # send notification email to the maintainer
+ # send notification email to the maintainers
t = loader.get_template('packages/outofdate.txt')
c = Context({
'email': form.cleaned_data['email'],
'message': form.cleaned_data['usermessage'],
'pkg': pkg,
- 'weburl': pkg.get_full_url(),
+ 'packages': flagged_pkgs,
})
send_mail(subject,
t.render(c),
@@ -400,14 +411,30 @@ def flag(request, name, repo, arch):
toemail,
fail_silently=True)
- context['confirmed'] = True
+ return redirect('package-flag-confirmed', name=name, repo=repo,
+ arch=arch)
else:
form = FlagForm()
- context['form'] = form
-
+ context = {
+ 'package': pkg,
+ 'packages': pkgs,
+ 'form': form
+ }
return direct_to_template(request, 'packages/flag.html', context)
+def flag_confirmed(request, name, repo, arch):
+ pkg = get_object_or_404(Package,
+ pkgname=name, repo__name__iexact=repo, arch__name=arch)
+ pkgs = Package.objects.select_related('arch', 'repo').filter(
+ pkgbase=pkg.pkgbase, flag_date=pkg.flag_date,
+ repo__testing=pkg.repo.testing).order_by(
+ 'pkgname', 'repo__name', 'arch__name')
+
+ context = {'package': pkg, 'packages': pkgs}
+
+ return direct_to_template(request, 'packages/flag_confirmed.html', context)
+
def download(request, name, repo, arch):
pkg = get_object_or_404(Package,
pkgname=name, repo__name__iexact=repo, arch__name=arch)
@@ -418,13 +445,13 @@ def download(request, name, repo, arch):
if pkg.arch.agnostic:
# grab the first non-any arch to fake the download path
arch = Arch.objects.exclude(agnostic=True)[0].name
- details = {
+ values = {
'host': mirrorurl.url,
'arch': arch,
'repo': pkg.repo.name.lower(),
'file': pkg.filename,
}
- url = string.Template('${host}${repo}/os/${arch}/${file}').substitute(details)
+ url = string.Template('${host}${repo}/os/${arch}/${file}').substitute(values)
return redirect(url)
def arch_differences(request):
@@ -440,6 +467,7 @@ def arch_differences(request):
return direct_to_template(request, 'packages/differences.html', context)
@permission_required('main.change_package')
+@never_cache
def stale_relations(request):
relations = PackageRelation.objects.select_related('user')
pkgbases = Package.objects.all().values('pkgbase')
diff --git a/public/utils.py b/public/utils.py
index 8ce2af45..fd29a845 100644
--- a/public/utils.py
+++ b/public/utils.py
@@ -3,6 +3,51 @@ from operator import attrgetter
from main.models import Arch, Package
from main.utils import cache_function
+class RecentUpdate(object):
+ def __init__(self, packages):
+ if len(packages) == 0:
+ raise Exception
+ first = packages[0]
+ self.pkgbase = first.pkgbase
+ self.repo = first.repo
+ self.version = ''
+
+ packages = sorted(packages, key=attrgetter('arch', 'pkgname'))
+ # split the packages into two lists. we need to prefer packages
+ # matching pkgbase as our primary, and group everything else in other.
+ self.packages = [pkg for pkg in packages if pkg.pkgname == pkg.pkgbase]
+ self.others = [pkg for pkg in packages if pkg.pkgname != pkg.pkgbase]
+
+ if self.packages:
+ version = self.packages[0].full_version
+ if all(version == pkg.full_version for pkg in self.packages):
+ self.version = version
+ elif self.others:
+ version = self.others[0].full_version
+ if all(version == pkg.full_version for pkg in self.others):
+ self.version = version
+
+ def package_links(self):
+ '''Returns either actual packages or package-standins for virtual
+ pkgbase packages.'''
+ if self.packages:
+ # we have real packages- just yield each in sequence
+ for package in self.packages:
+ yield package
+ else:
+ # time to fake out the template, this is a tad dirty
+ arches = set(pkg.arch for pkg in self.others)
+ for arch in arches:
+ url = '/packages/%s/%s/%s/' % (
+ self.repo.name.lower(), arch.name, self.pkgbase)
+ package_stub = {
+ 'pkgname': self.pkgbase,
+ 'arch': arch,
+ 'repo': self.repo,
+ 'get_absolute_url': url
+ }
+ yield package_stub
+
@cache_function(300)
def get_recent_updates(number=15):
# This is a bit of magic. We are going to show 15 on the front page, but we
@@ -10,24 +55,26 @@ def get_recent_updates(number=15):
# packages that we can later do some screening and trim out the fat.
pkgs = []
# grab a few extra so we can hopefully catch everything we need
- fetch = number * 4
+ fetch = number * 6
for arch in Arch.objects.all():
pkgs += list(Package.objects.select_related(
'arch', 'repo').filter(arch=arch).order_by('-last_update')[:fetch])
pkgs.sort(key=attrgetter('last_update'))
+
updates = []
- ctr = 0
- while ctr < number and len(pkgs) > 0:
- # not particularly happy with this logic, but it works.
- p = pkgs.pop()
- is_same = lambda q: p.is_same_version(q) and p.repo == q.repo
- samepkgs = filter(is_same, pkgs)
- samepkgs.append(p)
- samepkgs.sort(key=attrgetter('arch'))
- updates.append(samepkgs)
- for q in samepkgs:
- if p != q: pkgs.remove(q)
- ctr += 1
- return updates
+ while len(pkgs) > 0:
+ pkg = pkgs.pop()
+
+ in_group = lambda x: pkg.repo == x.repo and pkg.pkgbase == x.pkgbase
+ samepkgs = [other for other in pkgs if in_group(other)]
+ samepkgs.append(pkg)
+
+ # now remove all the packages we just pulled out
+ pkgs = [other for other in pkgs if other not in samepkgs]
+
+ update = RecentUpdate(samepkgs)
+ updates.append(update)
+
+ return updates[:number]
# vim: set ts=4 sw=4 et:
diff --git a/releng/__init__.py b/releng/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/releng/__init__.py
diff --git a/releng/admin.py b/releng/admin.py
new file mode 100644
index 00000000..be5e211f
--- /dev/null
+++ b/releng/admin.py
@@ -0,0 +1,31 @@
+from django.contrib import admin
+
+from .models import (Architecture, BootType, Bootloader, ClockChoice,
+ Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source,
+ Test)
+
+class IsoAdmin(admin.ModelAdmin):
+ list_display = ('name', 'created', 'active')
+ list_filter = ('active',)
+
+class TestAdmin(admin.ModelAdmin):
+ list_display = ('user_name', 'user_email', 'created', 'ip_address',
+ 'iso', 'success')
+ list_filter = ('success', 'iso')
+
+
+admin.site.register(Architecture)
+admin.site.register(BootType)
+admin.site.register(Bootloader)
+admin.site.register(ClockChoice)
+admin.site.register(Filesystem)
+admin.site.register(HardwareType)
+admin.site.register(InstallType)
+admin.site.register(IsoType)
+admin.site.register(Module)
+admin.site.register(Source)
+
+admin.site.register(Iso, IsoAdmin)
+admin.site.register(Test, TestAdmin)
+
+# vim: set ts=4 sw=4 et:
diff --git a/releng/fixtures/architecture.json b/releng/fixtures/architecture.json
new file mode 100644
index 00000000..0bf9b8bf
--- /dev/null
+++ b/releng/fixtures/architecture.json
@@ -0,0 +1,30 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.architecture",
+ "fields": {
+ "name": "dual, option i686"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.architecture",
+ "fields": {
+ "name": "dual, option x86_64"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.architecture",
+ "fields": {
+ "name": "i686"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "releng.architecture",
+ "fields": {
+ "name": "x86_64"
+ }
+ }
+]
diff --git a/releng/fixtures/bootloaders.json b/releng/fixtures/bootloaders.json
new file mode 100644
index 00000000..bee02f2b
--- /dev/null
+++ b/releng/fixtures/bootloaders.json
@@ -0,0 +1,23 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.bootloader",
+ "fields": {
+ "name": "grub"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.bootloader",
+ "fields": {
+ "name": "syslinux"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.bootloader",
+ "fields": {
+ "name": "other/manual"
+ }
+ }
+]
diff --git a/releng/fixtures/boottype.json b/releng/fixtures/boottype.json
new file mode 100644
index 00000000..ed4636eb
--- /dev/null
+++ b/releng/fixtures/boottype.json
@@ -0,0 +1,23 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.boottype",
+ "fields": {
+ "name": "optical"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.boottype",
+ "fields": {
+ "name": "usb"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.boottype",
+ "fields": {
+ "name": "pxe"
+ }
+ }
+]
diff --git a/releng/fixtures/clockchoices.json b/releng/fixtures/clockchoices.json
new file mode 100644
index 00000000..f328801a
--- /dev/null
+++ b/releng/fixtures/clockchoices.json
@@ -0,0 +1,23 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "unchanged"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "configured manually"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "NTP"
+ }
+ }
+]
diff --git a/releng/fixtures/filesystems.json b/releng/fixtures/filesystems.json
new file mode 100644
index 00000000..208f5c73
--- /dev/null
+++ b/releng/fixtures/filesystems.json
@@ -0,0 +1,23 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.filesystem",
+ "fields": {
+ "name": "autoprepare"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.filesystem",
+ "fields": {
+ "name": "manual"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.filesystem",
+ "fields": {
+ "name": "from config file"
+ }
+ }
+]
diff --git a/releng/fixtures/hardware.json b/releng/fixtures/hardware.json
new file mode 100644
index 00000000..a2bb9ec0
--- /dev/null
+++ b/releng/fixtures/hardware.json
@@ -0,0 +1,44 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.hardwaretype",
+ "fields": {
+ "name": "virtualbox"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.hardwaretype",
+ "fields": {
+ "name": "qemu"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.hardwaretype",
+ "fields": {
+ "name": "intel i686"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "releng.hardwaretype",
+ "fields": {
+ "name": "intel x86_64"
+ }
+ },
+ {
+ "pk": 5,
+ "model": "releng.hardwaretype",
+ "fields": {
+ "name": "amd i686"
+ }
+ },
+ {
+ "pk": 6,
+ "model": "releng.hardwaretype",
+ "fields": {
+ "name": "amd x86_64"
+ }
+ }
+]
diff --git a/releng/fixtures/installtype.json b/releng/fixtures/installtype.json
new file mode 100644
index 00000000..07d17f28
--- /dev/null
+++ b/releng/fixtures/installtype.json
@@ -0,0 +1,30 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.installtype",
+ "fields": {
+ "name": "interactive install"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.installtype",
+ "fields": {
+ "name": "automatic install generic example"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.installtype",
+ "fields": {
+ "name": "automatic install fancy example"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "releng.installtype",
+ "fields": {
+ "name": "automatic install custom config (if special, specify in comments)"
+ }
+ }
+]
diff --git a/releng/fixtures/isotypes.json b/releng/fixtures/isotypes.json
new file mode 100644
index 00000000..a529b181
--- /dev/null
+++ b/releng/fixtures/isotypes.json
@@ -0,0 +1,16 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.isotype",
+ "fields": {
+ "name": "core"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.isotype",
+ "fields": {
+ "name": "net"
+ }
+ }
+]
diff --git a/releng/fixtures/modules.json b/releng/fixtures/modules.json
new file mode 100644
index 00000000..9cdf1a8d
--- /dev/null
+++ b/releng/fixtures/modules.json
@@ -0,0 +1,86 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.module",
+ "fields": {
+ "name": "lvm2"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.module",
+ "fields": {
+ "name": "dm_crypt"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.module",
+ "fields": {
+ "name": "softraid"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "releng.module",
+ "fields": {
+ "name": "nilfs2"
+ }
+ },
+ {
+ "pk": 5,
+ "model": "releng.module",
+ "fields": {
+ "name": "btrfs"
+ }
+ },
+ {
+ "pk": 6,
+ "model": "releng.module",
+ "fields": {
+ "name": "ext2"
+ }
+ },
+ {
+ "pk": 7,
+ "model": "releng.module",
+ "fields": {
+ "name": "ext3"
+ }
+ },
+ {
+ "pk": 8,
+ "model": "releng.module",
+ "fields": {
+ "name": "ext4"
+ }
+ },
+ {
+ "pk": 9,
+ "model": "releng.module",
+ "fields": {
+ "name": "swap"
+ }
+ },
+ {
+ "pk": 10,
+ "model": "releng.module",
+ "fields": {
+ "name": "xfs"
+ }
+ },
+ {
+ "pk": 11,
+ "model": "releng.module",
+ "fields": {
+ "name": "jfs"
+ }
+ },
+ {
+ "pk": 12,
+ "model": "releng.module",
+ "fields": {
+ "name": "reiserFS"
+ }
+ }
+]
diff --git a/releng/fixtures/source.json b/releng/fixtures/source.json
new file mode 100644
index 00000000..9d1950a5
--- /dev/null
+++ b/releng/fixtures/source.json
@@ -0,0 +1,23 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.source",
+ "fields": {
+ "name": "net install manual networking config (verify network, rc.conf, resolv.conf, mirrorlist)"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.source",
+ "fields": {
+ "name": "net install dhcp (verify network, rc.conf, mirrorlist)"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.source",
+ "fields": {
+ "name": "core"
+ }
+ }
+]
diff --git a/releng/management/__init__.py b/releng/management/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/releng/management/__init__.py
diff --git a/releng/management/commands/__init__.py b/releng/management/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/releng/management/commands/__init__.py
diff --git a/releng/management/commands/syncisos.py b/releng/management/commands/syncisos.py
new file mode 100644
index 00000000..ba174131
--- /dev/null
+++ b/releng/management/commands/syncisos.py
@@ -0,0 +1,51 @@
+import re
+import urllib
+from HTMLParser import HTMLParser, HTMLParseError
+
+from django.conf import settings
+from django.core.management.base import BaseCommand, CommandError
+
+from releng.models import Iso
+
+class IsoListParser(HTMLParser):
+ def __init__(self):
+ HTMLParser.__init__(self)
+
+ self.hyperlinks = []
+ self.url_re = re.compile('(?!\.{2})/$')
+
+ def handle_starttag(self, tag, attrs):
+ if tag == 'a':
+ for name, value in attrs:
+ if name == "href":
+ if value != '../' and self.url_re.search(value) != None:
+ self.hyperlinks.append(value[:-1])
+
+ def parse(self, url):
+ try:
+ remote_file = urllib.urlopen(url)
+ data = remote_file.read()
+ remote_file.close()
+ self.feed(data)
+ self.close()
+ return self.hyperlinks
+ except HTMLParseError:
+ raise CommandError('Couldn\'t parse "%s"' % url)
+
+class Command(BaseCommand):
+ help = 'Gets new isos from %s' % settings.ISO_LIST_URL
+
+ def handle(self, *args, **options):
+ parser = IsoListParser()
+ isonames = Iso.objects.values_list('name', flat=True)
+ active_isos = parser.parse(settings.ISO_LIST_URL)
+
+ # create any names that don't already exist
+ for iso in active_isos:
+ if iso not in isonames:
+ new = Iso(name=iso, active=True)
+ new.save()
+ # and then mark all other names as no longer active
+ Iso.objects.exclude(name__in=active_isos).update(active=False)
+
+# vim: set ts=4 sw=4 et:
diff --git a/releng/migrations/0001_initial.py b/releng/migrations/0001_initial.py
new file mode 100644
index 00000000..91fab8b7
--- /dev/null
+++ b/releng/migrations/0001_initial.py
@@ -0,0 +1,258 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'Iso'
+ db.create_table('releng_iso', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('created', self.gf('django.db.models.fields.DateTimeField')()),
+ ('active', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ))
+ db.send_create_signal('releng', ['Iso'])
+
+ # Adding model 'Architecture'
+ db.create_table('releng_architecture', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=200)),
+ ))
+ db.send_create_signal('releng', ['Architecture'])
+
+ # Adding model 'IsoType'
+ db.create_table('releng_isotype', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=200)),
+ ))
+ db.send_create_signal('releng', ['IsoType'])
+
+ # Adding model 'BootType'
+ db.create_table('releng_boottype', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=200)),
+ ))
+ db.send_create_signal('releng', ['BootType'])
+
+ # Adding model 'HardwareType'
+ db.create_table('releng_hardwaretype', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=200)),
+ ))
+ db.send_create_signal('releng', ['HardwareType'])
+
+ # Adding model 'InstallType'
+ db.create_table('releng_installtype', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=200)),
+ ))
+ db.send_create_signal('releng', ['InstallType'])
+
+ # Adding model 'Source'
+ db.create_table('releng_source', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=200)),
+ ))
+ db.send_create_signal('releng', ['Source'])
+
+ # Adding model 'ClockChoice'
+ db.create_table('releng_clockchoice', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=200)),
+ ))
+ db.send_create_signal('releng', ['ClockChoice'])
+
+ # Adding model 'Filesystem'
+ db.create_table('releng_filesystem', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=200)),
+ ))
+ db.send_create_signal('releng', ['Filesystem'])
+
+ # Adding model 'Module'
+ db.create_table('releng_module', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=200)),
+ ))
+ db.send_create_signal('releng', ['Module'])
+
+ # Adding model 'Bootloader'
+ db.create_table('releng_bootloader', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=200)),
+ ))
+ db.send_create_signal('releng', ['Bootloader'])
+
+ # Adding model 'Test'
+ db.create_table('releng_test', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('user_name', self.gf('django.db.models.fields.CharField')(max_length=500)),
+ ('user_email', self.gf('django.db.models.fields.EmailField')(max_length=75)),
+ ('ip_address', self.gf('django.db.models.fields.IPAddressField')(max_length=15)),
+ ('created', self.gf('django.db.models.fields.DateTimeField')()),
+ ('iso', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.Iso'])),
+ ('architecture', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.Architecture'])),
+ ('iso_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.IsoType'])),
+ ('boot_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.BootType'])),
+ ('hardware_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.HardwareType'])),
+ ('install_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.InstallType'])),
+ ('source', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.Source'])),
+ ('clock_choice', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.ClockChoice'])),
+ ('filesystem', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.Filesystem'])),
+ ('bootloader', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.Bootloader'])),
+ ('rollback_filesystem', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='rollback_test_set', null=True, to=orm['releng.Filesystem'])),
+ ('success', self.gf('django.db.models.fields.BooleanField')(default=False)),
+ ('comments', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+ ))
+ db.send_create_signal('releng', ['Test'])
+
+ # Adding M2M table for field modules on 'Test'
+ db.create_table('releng_test_modules', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('test', models.ForeignKey(orm['releng.test'], null=False)),
+ ('module', models.ForeignKey(orm['releng.module'], null=False))
+ ))
+ db.create_unique('releng_test_modules', ['test_id', 'module_id'])
+
+ # Adding M2M table for field rollback_modules on 'Test'
+ db.create_table('releng_test_rollback_modules', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('test', models.ForeignKey(orm['releng.test'], null=False)),
+ ('module', models.ForeignKey(orm['releng.module'], null=False))
+ ))
+ db.create_unique('releng_test_rollback_modules', ['test_id', 'module_id'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'Iso'
+ db.delete_table('releng_iso')
+
+ # Deleting model 'Architecture'
+ db.delete_table('releng_architecture')
+
+ # Deleting model 'IsoType'
+ db.delete_table('releng_isotype')
+
+ # Deleting model 'BootType'
+ db.delete_table('releng_boottype')
+
+ # Deleting model 'HardwareType'
+ db.delete_table('releng_hardwaretype')
+
+ # Deleting model 'InstallType'
+ db.delete_table('releng_installtype')
+
+ # Deleting model 'Source'
+ db.delete_table('releng_source')
+
+ # Deleting model 'ClockChoice'
+ db.delete_table('releng_clockchoice')
+
+ # Deleting model 'Filesystem'
+ db.delete_table('releng_filesystem')
+
+ # Deleting model 'Module'
+ db.delete_table('releng_module')
+
+ # Deleting model 'Bootloader'
+ db.delete_table('releng_bootloader')
+
+ # Deleting model 'Test'
+ db.delete_table('releng_test')
+
+ # Removing M2M table for field modules on 'Test'
+ db.delete_table('releng_test_modules')
+
+ # Removing M2M table for field rollback_modules on 'Test'
+ db.delete_table('releng_test_rollback_modules')
+
+
+ models = {
+ 'releng.architecture': {
+ 'Meta': {'object_name': 'Architecture'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+ },
+ 'releng.bootloader': {
+ 'Meta': {'object_name': 'Bootloader'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+ },
+ 'releng.boottype': {
+ 'Meta': {'object_name': 'BootType'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+ },
+ 'releng.clockchoice': {
+ 'Meta': {'object_name': 'ClockChoice'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+ },
+ 'releng.filesystem': {
+ 'Meta': {'object_name': 'Filesystem'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+ },
+ 'releng.hardwaretype': {
+ 'Meta': {'object_name': 'HardwareType'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+ },
+ 'releng.installtype': {
+ 'Meta': {'object_name': 'InstallType'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+ },
+ 'releng.iso': {
+ 'Meta': {'object_name': 'Iso'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'releng.isotype': {
+ 'Meta': {'object_name': 'IsoType'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+ },
+ 'releng.module': {
+ 'Meta': {'object_name': 'Module'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+ },
+ 'releng.source': {
+ 'Meta': {'object_name': 'Source'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+ },
+ 'releng.test': {
+ 'Meta': {'object_name': 'Test'},
+ 'architecture': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.Architecture']"}),
+ 'boot_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.BootType']"}),
+ 'bootloader': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.Bootloader']"}),
+ 'clock_choice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.ClockChoice']"}),
+ 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'filesystem': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.Filesystem']"}),
+ 'hardware_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.HardwareType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'install_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.InstallType']"}),
+ 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'iso': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.Iso']"}),
+ 'iso_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.IsoType']"}),
+ 'modules': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['releng.Module']", 'null': 'True', 'blank': 'True'}),
+ 'rollback_filesystem': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'rollback_test_set'", 'null': 'True', 'to': "orm['releng.Filesystem']"}),
+ 'rollback_modules': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rollback_test_set'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['releng.Module']"}),
+ 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.Source']"}),
+ 'success': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
+ 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '500'})
+ }
+ }
+
+ complete_apps = ['releng']
diff --git a/releng/migrations/__init__.py b/releng/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/releng/migrations/__init__.py
diff --git a/releng/models.py b/releng/models.py
new file mode 100644
index 00000000..07ede1c5
--- /dev/null
+++ b/releng/models.py
@@ -0,0 +1,120 @@
+from datetime import datetime
+
+from django.db import models
+
+class IsoOption(models.Model):
+ name = models.CharField(max_length=200)
+
+ def __unicode__(self):
+ return self.name
+
+ def get_test_result(self, success):
+ try:
+ return self.test_set.filter(success=success).select_related(
+ 'iso').latest('iso__id').iso
+ except Test.DoesNotExist:
+ return None
+
+ def get_last_success(self):
+ return self.get_test_result(True)
+
+ def get_last_failure(self):
+ return self.get_test_result(False)
+
+ class Meta:
+ abstract = True
+
+class RollbackOption(IsoOption):
+ def get_rollback_test_result(self, success):
+ try:
+ return self.rollback_test_set.filter(success=success).select_related(
+ 'iso').latest('iso__id').iso
+ except Test.DoesNotExist:
+ return None
+
+ def get_last_rollback_success(self):
+ return self.get_rollback_test_result(True)
+
+ def get_last_rollback_failure(self):
+ return self.get_rollback_test_result(False)
+
+ class Meta:
+ abstract = True
+
+class Iso(models.Model):
+ name = models.CharField(max_length=255)
+ created = models.DateTimeField(editable=False)
+ active = models.BooleanField(default=True)
+
+ def __unicode__(self):
+ return self.name
+
+class Architecture(IsoOption):
+ pass
+
+class IsoType(IsoOption):
+ pass
+
+class BootType(IsoOption):
+ pass
+
+class HardwareType(IsoOption):
+ pass
+
+class InstallType(IsoOption):
+ pass
+
+class Source(IsoOption):
+ pass
+
+class ClockChoice(IsoOption):
+ pass
+
+class Filesystem(RollbackOption):
+ pass
+
+class Module(RollbackOption):
+ pass
+
+class Bootloader(IsoOption):
+ pass
+
+class Test(models.Model):
+ user_name = models.CharField(max_length=500)
+ user_email = models.EmailField()
+ ip_address = models.IPAddressField()
+ created = models.DateTimeField(editable=False)
+
+ iso = models.ForeignKey(Iso)
+ architecture = models.ForeignKey(Architecture)
+ iso_type = models.ForeignKey(IsoType)
+ boot_type = models.ForeignKey(BootType)
+ hardware_type = models.ForeignKey(HardwareType)
+ install_type = models.ForeignKey(InstallType)
+ source = models.ForeignKey(Source)
+ clock_choice = models.ForeignKey(ClockChoice)
+ filesystem = models.ForeignKey(Filesystem)
+ modules = models.ManyToManyField(Module, null=True, blank=True)
+ bootloader = models.ForeignKey(Bootloader)
+ rollback_filesystem = models.ForeignKey(Filesystem,
+ related_name="rollback_test_set", null=True, blank=True)
+ rollback_modules = models.ManyToManyField(Module,
+ related_name="rollback_test_set", null=True, blank=True)
+
+ success = models.BooleanField()
+ comments = models.TextField(null=True, blank=True)
+
+def set_created_field(sender, **kwargs):
+ # We use this same callback for both Isos and Tests
+ obj = kwargs['instance']
+ if not obj.created:
+ obj.created = datetime.utcnow()
+
+from django.db.models.signals import pre_save
+
+pre_save.connect(set_created_field, sender=Iso,
+ dispatch_uid="releng.models")
+pre_save.connect(set_created_field, sender=Test,
+ dispatch_uid="releng.models")
+
+# vim: set ts=4 sw=4 et:
diff --git a/releng/urls.py b/releng/urls.py
new file mode 100644
index 00000000..4a125dff
--- /dev/null
+++ b/releng/urls.py
@@ -0,0 +1,14 @@
+from django.conf.urls.defaults import include, patterns
+
+feedback_patterns = patterns('releng.views',
+ (r'^$', 'test_results_overview', {}, 'releng-test-overview'),
+ (r'^submit/$', 'submit_test_result', {}, 'releng-test-submit'),
+ (r'^thanks/$', 'submit_test_thanks', {}, 'releng-test-thanks'),
+ (r'^iso/(?P<iso_id>\d+)/$', 'test_results_iso', {}, 'releng-results-iso'),
+ (r'^(?P<option>.+)/(?P<value>\d+)/$','test_results_for', {}, 'releng-results-for'),
+)
+
+urlpatterns = patterns('',
+ (r'^feedback/', include(feedback_patterns)),
+)
+# vim: set ts=4 sw=4 et:
diff --git a/releng/views.py b/releng/views.py
new file mode 100644
index 00000000..a810bbbc
--- /dev/null
+++ b/releng/views.py
@@ -0,0 +1,136 @@
+from django import forms
+from django.conf import settings
+from django.http import Http404
+from django.shortcuts import get_object_or_404, redirect
+from django.views.generic.simple import direct_to_template
+
+from .models import (Architecture, BootType, Bootloader, ClockChoice,
+ Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source,
+ Test)
+
+def standard_field(model, empty_label=None, help_text=None, required=True):
+ return forms.ModelChoiceField(queryset=model.objects.all(),
+ widget=forms.RadioSelect(), empty_label=empty_label,
+ help_text=help_text, required=required)
+
+class TestForm(forms.ModelForm):
+ iso = forms.ModelChoiceField(queryset=Iso.objects.filter(active=True))
+ architecture = standard_field(Architecture)
+ iso_type = standard_field(IsoType)
+ boot_type = standard_field(BootType)
+ hardware_type = standard_field(HardwareType)
+ install_type = standard_field(InstallType)
+ source = standard_field(Source)
+ clock_choice = standard_field(ClockChoice)
+ filesystem = standard_field(Filesystem,
+ help_text="verify /etc/fstab, `df -hT` output and commands like " \
+ "lvdisplay for special modules")
+ modules = forms.ModelMultipleChoiceField(queryset=Module.objects.all(),
+ help_text="", widget=forms.CheckboxSelectMultiple(), required=False)
+ bootloader = standard_field(Bootloader,
+ help_text="Verify that the entries in the bootloader config look ok")
+ rollback_filesystem = standard_field(Filesystem,
+ help_text="If you did a rollback followed by a new attempt to setup " \
+ "your blockdevices/filesystems, select which option you took here.",
+ empty_label="N/A (did not rollback)", required=False)
+ rollback_modules = forms.ModelMultipleChoiceField(queryset=Module.objects.all(),
+ help_text="If you did a rollback followed by a new attempt to setup " \
+ "your blockdevices/filesystems, select which option you took here.",
+ widget=forms.CheckboxSelectMultiple(), required=False)
+ success = forms.BooleanField(help_text="Only check this if everything went fine. " \
+ "If you you ran into any errors please specify them in the " \
+ "comments.", required=False)
+ website = forms.CharField(label='',
+ widget=forms.TextInput(attrs={'style': 'display:none;'}), required=False)
+
+ class Meta:
+ model = Test
+ fields = ("user_name", "user_email", "iso", "architecture",
+ "iso_type", "boot_type", "hardware_type",
+ "install_type", "source", "clock_choice", "filesystem",
+ "modules", "bootloader", "rollback_filesystem",
+ "rollback_modules", "success", "comments")
+ widgets = {
+ "modules": forms.CheckboxSelectMultiple(),
+ }
+
+def submit_test_result(request):
+ if request.POST:
+ form = TestForm(request.POST)
+ if form.is_valid() and request.POST['website'] == '':
+ test = form.save(commit=False)
+ test.ip_address = request.META.get("REMOTE_ADDR", None)
+ test.save()
+ form.save_m2m()
+ return redirect('releng-test-thanks')
+ else:
+ form = TestForm()
+
+ context = {'form': form}
+ return direct_to_template(request, 'releng/add.html', context)
+
+def calculate_option_overview(field_name):
+ field = Test._meta.get_field(field_name)
+ model = field.rel.to
+ is_rollback = field_name.startswith('rollback_')
+ option = {
+ 'option': model,
+ 'name': field_name,
+ 'is_rollback': is_rollback,
+ 'values': []
+ }
+ for value in model.objects.all():
+ data = { 'value': value }
+ if is_rollback:
+ data['success'] = value.get_last_rollback_success()
+ data['failure'] = value.get_last_rollback_failure()
+ else:
+ data['success'] = value.get_last_success()
+ data['failure'] = value.get_last_failure()
+ option['values'].append(data)
+
+ return option
+
+def test_results_overview(request):
+ # data structure produced:
+ # [ { option, name, is_rollback, values: [ { value, success, failure } ... ] } ... ]
+ all_options = []
+ fields = [ 'architecture', 'iso_type', 'boot_type', 'hardware_type',
+ 'install_type', 'source', 'clock_choice', 'filesystem', 'modules',
+ 'bootloader', 'rollback_filesystem', 'rollback_modules' ]
+ for field in fields:
+ all_options.append(calculate_option_overview(field))
+
+ context = {
+ 'options': all_options,
+ 'iso_url': settings.ISO_LIST_URL,
+ }
+ return direct_to_template(request, 'releng/results.html', context)
+
+def test_results_iso(request, iso_id):
+ iso = get_object_or_404(Iso, pk=iso_id)
+ test_list = iso.test_set.all()
+ context = {
+ 'iso_name': iso.name,
+ 'test_list': test_list
+ }
+ return direct_to_template(request, 'releng/result_list.html', context)
+
+def test_results_for(request, option, value):
+ if option not in Test._meta.get_all_field_names():
+ raise Http404
+ option_model = getattr(Test, option).field.rel.to
+ real_value = get_object_or_404(option_model, pk=value)
+ test_list = real_value.test_set.order_by("iso__name", "pk")
+ context = {
+ 'option': option,
+ 'value': real_value,
+ 'value_id': value,
+ 'test_list': test_list
+ }
+ return direct_to_template(request, 'releng/result_list.html', context)
+
+def submit_test_thanks(request):
+ return direct_to_template(request, "releng/thanks.html", None)
+
+# vim: set ts=4 sw=4 et:
diff --git a/requirements.txt b/requirements.txt
index 0a746d96..9be5d88e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-Django==1.2.5
+Django==1.3
Markdown==2.0.3
South==0.7.3
-pytz>=2010o
+pytz>=2011c
diff --git a/requirements_prod.txt b/requirements_prod.txt
index d9875667..babf65f9 100644
--- a/requirements_prod.txt
+++ b/requirements_prod.txt
@@ -1,6 +1,6 @@
-Django==1.2.5
+Django==1.3
Markdown==2.0.3
-MySQL-python==1.2.3c1
+MySQL-python==1.2.3
South==0.7.3
python-memcached==1.47
-pytz>=2010o
+pytz>=2011c
diff --git a/settings.py b/settings.py
index 029739f2..c9ea4178 100644
--- a/settings.py
+++ b/settings.py
@@ -104,6 +104,7 @@ INSTALLED_APPS = (
'devel',
'public',
'south', # database migration support
+ 'releng',
)
## Import local settings
@@ -123,4 +124,7 @@ if DEBUG_TOOLBAR:
INSTALLED_APPS = list(INSTALLED_APPS) + [ 'debug_toolbar' ]
+# URL to fetch a current list of available ISOs
+ISO_LIST_URL = 'http://releng.archlinux.org/isos/'
+
# vim: set ts=4 sw=4 et:
diff --git a/templates/devel/index.html b/templates/devel/index.html
index 11f73f3a..5913cdde 100644
--- a/templates/devel/index.html
+++ b/templates/devel/index.html
@@ -1,5 +1,7 @@
{% extends "base.html" %}
-{% block title %}Parabola - Developer Dashboard{% endblock %}
+{% load cache %}
+
+{% block title %}Parabola - Hacker Dashboard{% endblock %}
{% block content %}
<div id="dev-dashboard" class="box">
@@ -72,25 +74,44 @@
<tr>
<th>Name</th>
<th>Creation Date</th>
+ <th>Creator</th>
<th>Description</th>
+ <th>Package Count</th>
+ <th>Incomplete Count</th>
+ </tr>
</tr>
</thead>
<tbody>
{% for todo in todos %}
- <tr class="{% cycle 'odd' 'even' %}">
- <td><a href="{{ todo.get_absolute_url }}"
- title="View todo list: {{ todo.name }}">{{ todo.name }}</a></td>
- <td>{{ todo.date_added|date }}</td>
- <td class="wrap">{{ todo.description|urlize }}</td>
- </tr>
+ <tr class="{% cycle 'odd' 'even' %}">
+ <td><a href="{{ todo.get_absolute_url }}"
+ title="View todo list: {{ todo.name }}">{{ todo.name }}</a></td>
+ <td>{{ todo.date_added|date }}</td>
+ <td>{{ todo.creator.get_full_name }}</td>
+ <td class="wrap">{{ todo.description|urlize }}</td>
+ <td>{{ todo.pkg_count }}</td>
+ <td>{{ todo.incomplete_count }}</td>
+ </tr>
{% empty %}
- <tr class="empty"><td colspan="3"><em>No package todo lists to display</em></td></tr>
+ <tr class="empty"><td colspan="3"><em>No package todo lists to display</em></td></tr>
{% endfor %}
</tbody>
</table>
+ <h3>Developer Reports</h3>
+ <ul>
+ <li><a href="reports/big/">Big</a>: All packages with compressed size &gt; 50 MiB</li>
+ <li><a href="reports/old/">Old</a>: Packages last built more than two years ago</li>
+ <li><a href="reports/uncompressed-man/">Uncompressed Manpages</a>: Self-explanatory</li>
+ <li><a href="reports/uncompressed-info/">Uncompressed Info Pages</a>: Self-explanatory</li>
+ <li><a href="reports/unneeded-orphans/">Unneeded Orphans</a>: Packages
+ that have no maintainer and are not required by any other package in
+ any repository</li>
+ </ul>
+
</div><!-- #dev-dashboard -->
+{% cache 60 dev-dash-by-arch %}
<div id="dash-by-arch" class="box">
<h2>Stats by Architecture</h2>
@@ -117,9 +138,10 @@
{% endfor %}
</tbody>
</table>
+</div>{# #dash-by-arch #}
+{% endcache %}
-</div><!-- #dash-by-arch -->
-
+{% cache 60 dev-dash-by-repo %}
<div id="dash-by-repo" class="box">
<h2>Stats by Repository</h2>
@@ -146,9 +168,10 @@
{% endfor %}
</tbody>
</table>
+</div>{# dash-by-arch #}
+{% endcache %}
-</div><!-- dash-by-arch -->
-
+{% cache 60 dev-dash-by-maintainer %}
<div id="dash-by-maintainer" class="box">
<h2>Stats by Maintainer</h2>
@@ -184,8 +207,9 @@
{% endfor %}
</tbody>
</table>
+</div>{# #dash-by-maintainer #}
+{% endcache %}
-</div><!-- #dash-by-maintainer -->
{% load cdn %}{% jquery %}
<script type="text/javascript" src="/media/jquery.tablesorter.min.js"></script>
<script type="text/javascript" src="/media/archweb.js"></script>
diff --git a/templates/devel/packages.html b/templates/devel/packages.html
new file mode 100644
index 00000000..e0988c03
--- /dev/null
+++ b/templates/devel/packages.html
@@ -0,0 +1,58 @@
+{% extends "base.html" %}
+{% load attributes %}
+
+{% block title %}Arch Linux - {{ title }}{% endblock %}
+
+{% block content %}
+<div class="box">
+ <h2>{{ title }}</h2>
+ <p>{{ packages|length }} package{{ packages|pluralize }} found.</p>
+ <table class="results">
+ <thead>
+ <tr>
+ <th>Arch</th>
+ <th>Repo</th>
+ <th>Name</th>
+ <th>Version</th>
+ <th>Description</th>
+ <th>Last Updated</th>
+ <th>Build Date</th>
+ <th>Flag Date</th>
+ {% for name in column_names %}
+ <th>{{ name }}</th>
+ {% endfor %}
+ </tr>
+ </thead>
+ <tbody>
+ {% for pkg in packages %}
+ <tr class="{% cycle pkgr2,pkgr1 %}">
+ <td>{{ pkg.arch.name }}</td>
+ <td>{{ pkg.repo.name|capfirst }}</td>
+ <td><a href="{{ pkg.get_absolute_url }}"
+ title="Package details for {{ pkg.pkgname }}">{{ pkg.pkgname }}</a></td>
+ {% if pkg.flag_date %}
+ <td><span class="flagged">{{ pkg.full_version }}</span></td>
+ {% else %}
+ <td>{{ pkg.full_version }}</td>
+ {% endif %}
+ <td class="wrap">{{ pkg.pkgdesc }}</td>
+ <td>{{ pkg.last_update|date }}</td>
+ <td>{{ pkg.build_date|date }}</td>
+ <td>{{ pkg.flag_date|date }}</td>
+ {% for attr in column_attrs %}
+ <td>{{ pkg|attribute:attr }}</td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+</div>
+{% load cdn %}{% jquery %}
+<script type="text/javascript" src="/media/jquery.tablesorter.min.js"></script>
+<script type="text/javascript" src="/media/archweb.js"></script>
+<script type="text/javascript">
+$(document).ready(function() {
+ $(".results").tablesorter({widgets: ['zebra']});
+});
+</script>
+{% endblock %}
diff --git a/templates/feeds/news_description.html b/templates/feeds/news_description.html
index a1e6446f..e75d0af7 100644
--- a/templates/feeds/news_description.html
+++ b/templates/feeds/news_description.html
@@ -1,3 +1,3 @@
{% load markup %}
<p>{{obj.author.get_full_name}} wrote:</p>
-{{ obj.content|markdown }}
+{{ obj.content|markdown }} \ No newline at end of file
diff --git a/templates/feeds/news_title.html b/templates/feeds/news_title.html
index d355de5b..7899fce3 100644
--- a/templates/feeds/news_title.html
+++ b/templates/feeds/news_title.html
@@ -1 +1 @@
-{{ obj.title }}
+{{ obj.title }} \ No newline at end of file
diff --git a/templates/feeds/packages_description.html b/templates/feeds/packages_description.html
index 6b9c47b3..cfc42616 100644
--- a/templates/feeds/packages_description.html
+++ b/templates/feeds/packages_description.html
@@ -1 +1 @@
-{{ obj.pkgdesc }}
+{{ obj.pkgdesc }} \ No newline at end of file
diff --git a/templates/feeds/packages_title.html b/templates/feeds/packages_title.html
index 5c54ba65..f92ac684 100644
--- a/templates/feeds/packages_title.html
+++ b/templates/feeds/packages_title.html
@@ -1 +1 @@
-{{ obj.pkgname }} {{ obj.full_version }} {{ obj.arch.name }}
+{{ obj.pkgname }} {{ obj.full_version }} {{ obj.arch.name }} \ No newline at end of file
diff --git a/templates/mirrors/mirror_details.html b/templates/mirrors/mirror_details.html
index 2edc61d4..1b44f65b 100644
--- a/templates/mirrors/mirror_details.html
+++ b/templates/mirrors/mirror_details.html
@@ -1,4 +1,5 @@
{% extends "base.html" %}
+{% load mirror_status %}
{% block title %}Parabola - {{ mirror.name }} - Mirror Details{% endblock %}
@@ -12,47 +13,105 @@
<tr>
<th>Name:</th>
<td>{{ mirror.name }}</td>
- </tr><tr>
+ </tr>
+ <tr>
<th>Tier:</th>
<td>{{ mirror.get_tier_display }}</td>
- </tr><tr>
+ </tr>
+ <tr>
+ <th>Country:</th>
+ <td>{{ mirror.country }}</td>
+ </tr>
+ <tr>
+ <th>Has ISOs:</th>
+ <td>{{ mirror.isos|yesno }}</td>
+ </tr>
+ {% if user.is_authenticated %}
+ <tr>
+ <th>Public:</th>
+ <td>{{ mirror.public|yesno }}</td>
+ </tr>
+ <tr>
+ <th>Active:</th>
+ <td>{{ mirror.active|yesno }}</td>
+ </tr>
+ <tr>
+ <th>Rsync IPs:</th>
+ <td class="wrap">{{mirror.rsync_ips.all|join:', '}}</td>
+ </tr>
+ <tr>
+ <th>Admin Email:</th>
+ <td>{% if mirror.admin_email %}<a href="mailto:{{ mirror.admin_email }}">{{ mirror.admin_email }}</a>{% else %}None{% endif %}</td>
+ </tr>
+ <tr>
+ <th>Notes:</th>
+ <td>{{ mirror.notes|linebreaks }}</td>
+ </tr>
+ <tr>
<th>Upstream:</th>
- <!-- TODO: linking to non-public mirrors -->
<td>{% if mirror.upstream %}
<a href="{{ mirror.upstream.get_absolute_url }}"
title="Mirror details for {{ mirror.upstream.name }}">{{ mirror.upstream.name }}</a>
{% else %}None{% endif %}</td>
- </tr><tr>
+ </tr>
+ <tr>
<th>Downstream:</th>
{% with mirror.downstream as ds_mirrors %}
<td>{% if ds_mirrors %}
{% for ds in ds_mirrors %}
<a href="{{ ds.get_absolute_url }}"
- title="Mirror details for {{ ds.name }}">{{ ds.name }}</a><br/>
+ title="Mirror details for {{ ds.name }}">{{ ds.name }}</a>
+ {% if not ds.active %}<span class="testing-dep">(inactive)</span>{% endif %}
+ {% if not ds.public %}<span class="testing-dep">(private)</span>{% endif %}
+ <br/>
{% endfor %}
- {% else %}None{% endif %}
- </td>
- {% endwith %}
- </tr><tr>
- <th>Country:</th>
- <td>{{ mirror.country }}</td>
- </tr><tr>
- <th>Has ISOs:</th>
- <td>{{ mirror.isos|yesno }}</td>
- </tr><tr>
- <th>Protocols:</th>
- <td>{{ mirror.supported_protocols }}</td>
- </tr><tr>
- <th>Mirror URLs:</th>
- {% with mirror.urls.all as urls %}
- <td>{% if urls %}
- {% for u in urls %}
- <a href="{{ u.url }}">{{ u.url }}</a><br/>
- {% endfor %}
- {% else %}None{% endif %}
- </td>
+ {% else %}None{% endif %}</td>
{% endwith %}
</tr>
+ {% endif %}
+ </table>
+
+ <h3>Available URLs</h3>
+
+ <table id="available_urls" class="results">
+ <thead>
+ <tr>
+ <th>Mirror URL</th>
+ <th>IPv4</th>
+ <th>IPv6</th>
+ <th>Last Sync</th>
+ <th>Completion %</th>
+ <th>μ Delay (hh:mm)</th>
+ <th>μ Duration (secs)</th>
+ <th>σ Duration (secs)</th>
+ <th>Mirror Score</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for m_url in urls %}
+ <tr class="{% cycle 'odd' 'even' %}">
+ <td>{% if m_url.protocol.is_download %}<a href="{{ m_url.url }}">{{ m_url.url }}</a>{% else %}{{ m_url.url }}{% endif %}</td>
+ <td>{{ m_url.has_ipv4|yesno }}</td>
+ <td>{{ m_url.has_ipv6|yesno }}</td>
+ <td>{{ m_url.last_sync|date:'Y-m-d H:i'|default:'unknown' }}</td>
+ <td>{{ m_url.completion_pct|percentage:1 }}</td>
+ <td>{{ m_url.delay|duration|default:'unknown' }}</td>
+ <td>{{ m_url.duration_avg|floatformat:2 }}</td>
+ <td>{{ m_url.duration_stddev|floatformat:2 }}</td>
+ <td>{{ m_url.score|floatformat:1|default:'∞' }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
</table>
</div>
+{% load cdn %}{% jquery %}
+<script type="text/javascript" src="/media/jquery.tablesorter.min.js"></script>
+<script type="text/javascript" src="/media/archweb.js"></script>
+<script type="text/javascript">
+$(document).ready(function() {
+ $("#available_urls:has(tbody tr)").tablesorter(
+ {widgets: ['zebra'], sortList: [[0,0]],
+ headers: { 6: { sorter: 'mostlydigit' }, 7: { sorter: 'mostlydigit' }, 8: { sorter: 'mostlydigit' } } });
+});
+</script>
{% endblock %}
diff --git a/templates/mirrors/mirrorlist.txt b/templates/mirrors/mirrorlist.txt
index c5197e91..b91c52a2 100644
--- a/templates/mirrors/mirrorlist.txt
+++ b/templates/mirrors/mirrorlist.txt
@@ -8,6 +8,6 @@ content right, and then go back later to fix it all up.
## Generated on {% now "Y-m-d" %}
##{% for mirror_url in mirror_urls %}{% ifchanged %}
-## {{ mirror_url.mirror.country }}{% endifchanged %}
+## {{ mirror_url.real_country }}{% endifchanged %}
#Server = {{ mirror_url.url}}$repo/os/$arch{% endfor %}
{% endautoescape %}
diff --git a/templates/mirrors/mirrorlist_status.txt b/templates/mirrors/mirrorlist_status.txt
index dbc03911..5bf94287 100644
--- a/templates/mirrors/mirrorlist_status.txt
+++ b/templates/mirrors/mirrorlist_status.txt
@@ -8,6 +8,6 @@ content right, and then go back later to fix it all up.
## Sorted by mirror score from mirror status page
## Generated on {% now "Y-m-d" %}
{% for mirror_url in mirror_urls %}
-## Score: {{ mirror_url.score|floatformat:1|default:'unknown' }}, {{ mirror_url.mirror.country }}
+## Score: {{ mirror_url.score|floatformat:1|default:'unknown' }}, {{ mirror_url.real_country }}
#Server = {{ mirror_url.url}}$repo/os/$arch{% endfor %}
{% endautoescape %}
diff --git a/templates/mirrors/mirrors.html b/templates/mirrors/mirrors.html
index 7562cb6f..41cca6fa 100644
--- a/templates/mirrors/mirrors.html
+++ b/templates/mirrors/mirrors.html
@@ -15,7 +15,6 @@
{% if user.is_authenticated %}
<th>Public</th>
<th>Active</th>
- <th>Rsync IPs</th>
<th>Admin Email</th>
<th>Notes</th>
{% endif %}
@@ -33,7 +32,6 @@
{% if user.is_authenticated %}
<td>{{mirror.public|yesno}}</td>
<td>{{mirror.active|yesno}}</td>
- <td class="wrap">{{mirror.rsync_ips.all|join:', '}}</td>
<td>{{mirror.admin_email}}</td>
<td class="wrap">{{mirror.notes|linebreaks}}</td>
{% endif %}
diff --git a/templates/mirrors/status.html b/templates/mirrors/status.html
index cd56f8f9..f315f7c3 100644
--- a/templates/mirrors/status.html
+++ b/templates/mirrors/status.html
@@ -91,7 +91,7 @@
<tr class="{% cycle 'odd' 'even' %}">
<td>{{ log.url__url }}</td>
<td>{{ log.url__protocol__protocol }}</td>
- <td>{{ log.url__mirror__country }}</td>
+ <td>{{ log.country }}</td>
<td>{{ log.error }}</td>
<td>{{ log.last_occurred|date:'Y-m-d H:i' }}</td>
<td>{{ log.error_count }}</td>
@@ -106,12 +106,11 @@
<script type="text/javascript" src="/media/archweb.js"></script>
<script type="text/javascript">
$(document).ready(function() {
+ var headers = { 5: { sorter: 'duration' }, 6: { sorter: 'mostlydigit' }, 7: { sorter: 'mostlydigit' }, 8: { sorter: 'mostlydigit' } };
$("#outofsync_mirrors:has(tbody tr)").tablesorter(
- {widgets: ['zebra'], sortList: [[3,1]],
- headers: { 6: { sorter: 'mostlydigit' }, 7: { sorter: 'mostlydigit' }, 8: { sorter: 'mostlydigit' } } });
+ {widgets: ['zebra'], sortList: [[3,1]], headers: headers });
$("#successful_mirrors:has(tbody tr)").tablesorter(
- {widgets: ['zebra'], sortList: [[8,0]],
- headers: { 6: { sorter: 'mostlydigit' }, 7: { sorter: 'mostlydigit' }, 8: { sorter: 'mostlydigit' } } });
+ {widgets: ['zebra'], sortList: [[8,0]], headers: headers });
$("#errorlog_mirrors:has(tbody tr)").tablesorter(
{widgets: ['zebra'], sortList: [[4,1], [5,1]]});
});
diff --git a/templates/mirrors/status_table.html b/templates/mirrors/status_table.html
index 240a5452..72de25dc 100644
--- a/templates/mirrors/status_table.html
+++ b/templates/mirrors/status_table.html
@@ -18,7 +18,7 @@
<tr class="{% cycle 'odd' 'even' %}">
<td>{{ m_url.url }}</td>
<td>{{ m_url.protocol }}</td>
- <td>{{ m_url.mirror.country }}</td>
+ <td>{{ m_url.real_country }}</td>
<td>{{ m_url.last_sync|date:'Y-m-d H:i'|default:'unknown' }}</td>
<td>{{ m_url.completion_pct|percentage:1 }}</td>
<td>{{ m_url.delay|duration|default:'unknown' }}</td>
diff --git a/templates/packages/details.html b/templates/packages/details.html
index 83673867..a8b2346e 100644
--- a/templates/packages/details.html
+++ b/templates/packages/details.html
@@ -15,9 +15,10 @@
<div id="actionlist">
<h4>Package Actions</h4>
<ul class="small">
- <li><a href="{{ pkg.get_arch_svn_link }}" title="View SVN entries in the {{pkg.repo|lower}}-{{pkg.arch}} branch">SVN Entries ({{pkg.repo|lower}}-{{pkg.arch}})</a></li>
- <li><a href="{{ pkg.get_trunk_svn_link }}" title="View SVN entries on trunk">SVN Entries (trunk)</a></li>
- <li><a href="{{ pkg.get_bugs_link }}" title="View existing bug tickets for {{ pkg.pkgname }}">Bug Reports</a></li>
+ <li><a href="{% svn_arch pkg %}" title="View SVN entries in the {{pkg.repo|lower}}-{{pkg.arch}} branch">SVN Entries ({{pkg.repo|lower}}-{{pkg.arch}})</a></li>
+ <li><a href="{% svn_trunk pkg %}" title="View SVN entries on trunk">SVN Entries (trunk)</a></li>
+ <li><a href="{% bugs_list pkg %}" title="View existing bug tickets for {{ pkg.pkgname }}">Bug Reports</a></li>
+ <li><a href="{% bug_report pkg %}" title="Report bug for {{ pkg.pkgname }}">Report a Bug</a></li>
{% if pkg.flag_date %}
<li><span class="flagged">Flagged out-of-date on {{ pkg.flag_date|date }}</span></li>
{% with pkg.in_testing as tp %}{% if tp %}
@@ -103,7 +104,7 @@
{% endifequal %}
<tr>
<th>Description:</th>
- <td class="wrap">{% if pkg.pkgdesc %}{{ pkg.pkgdesc }}{% endif %}</td>
+ <td class="wrap">{{ pkg.pkgdesc|default:"" }}</td>
</tr><tr>
<th>Upstream URL:</th>
<td>{% if pkg.url %}<a href="{{ pkg.url }}"
diff --git a/templates/packages/flag.html b/templates/packages/flag.html
index 52c0444c..74f6982c 100644
--- a/templates/packages/flag.html
+++ b/templates/packages/flag.html
@@ -1,23 +1,22 @@
{% extends "base.html" %}
-{% block title %}Parabola - Flag Package - {{ pkg.pkgname }}{% endblock %}
+{% block title %}Parabola - Flag Package - {{ package.pkgname }}{% endblock %}
{% block navbarclass %}anb-packages{% endblock %}
{% block content %}
<div id="pkg-flag" class="box">
-{% if confirmed %}
- <h2>Package Flagged</h2>
-
- <p>Thank you, the maintainers have been notified about <strong>{{ pkg.pkgname }}</strong>.</p>
-
- <p>You can return to the package details page for
- <a href="{{ pkg.get_absolute_url }}" title="Package details for {{pkg.pkgname}}">{{pkg.pkgname}}</a>.</p>
-{% else %}
- <h2>Flag Package: {{ pkg.pkgname }}</h2>
+ <h2>Flag Package: {{ package.pkgname }}</h2>
<p>If you notice a package is out-of-date (i.e., there is a newer
<strong>stable</strong> release available), then please notify us using
the form below.</p>
+ <p>Note that all of the following packages will be marked out of date:</p>
+ <ul>
+ {% for pkg in packages %}
+ <li>{{ pkg.pkgname }} {{ pkg.full_version }} [{{ pkg.repo.name|lower }}] ({{ pkg.arch.name }})</li>
+ {% endfor %}
+ </ul>
+
<p>The message box portion of the flag utility is optional, and meant
for short messages only. If you need more than 200 characters for your
message, then file a bug report, email the maintainer directly, or send
@@ -26,17 +25,16 @@
with your additional text.</p>
<p><strong>Note:</strong> Please do <em>not</em> use this facility if the
- package is broken! Use the <a href="https://bugs.parabolagnulinux.org"
- title="Parabola Bugtracker">bug tracker</a> instead.</p>
+ package is broken! Please <a href="https://bugs.parabolagnulinux.org"
+ title="Parabola Bugtracker">file a bug</a> instead.</p>
- <p>Please confirm your flag request for {{pkg.pkgname}}:</p>
+ <p>Please confirm your flag request for {{package.pkgname}}:</p>
<form id="flag-pkg-form" method="post">{% csrf_token %}
<fieldset>
{{ form.as_p }}
</fieldset>
- <p><label></label> <input title="Flag {{ pkg.pkgname }} as out-of-date" type="submit" value="Flag Package" /></p>
+ <p><label></label> <input title="Flag {{ package.pkgname }} as out-of-date" type="submit" value="Flag Package" /></p>
</form>
-{% endif %}
</div>
{% endblock %}
diff --git a/templates/packages/flag_confirmed.html b/templates/packages/flag_confirmed.html
new file mode 100644
index 00000000..02c24f72
--- /dev/null
+++ b/templates/packages/flag_confirmed.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+{% block title %}Arch Linux - Package Flagged - {{ package.pkgname }}{% endblock %}
+{% block navbarclass %}anb-packages{% endblock %}
+
+{% block content %}
+<div id="pkg-flag" class="box">
+ <h2>Package Flagged - {{ package.pkgname }}</h2>
+
+ <p>Thank you, the maintainers have been notified the following packages are out-of-date:</p>
+ <ul>
+ {% for pkg in packages %}
+ <li>{{ pkg.pkgname }} {{ pkg.full_version }} [{{ pkg.repo.name|lower }}] ({{ pkg.arch.name }})</li>
+ {% endfor %}
+ </ul>
+
+ <p>You can return to the package details page for
+ <a href="{{ package.get_absolute_url }}" title="Package details for {{package.pkgname}}">{{package.pkgname}}</a>.</p>
+</div>
+{% endblock %}
diff --git a/templates/packages/outofdate.txt b/templates/packages/outofdate.txt
index 93abea03..4876c316 100644
--- a/templates/packages/outofdate.txt
+++ b/templates/packages/outofdate.txt
@@ -1,9 +1,7 @@
-{% autoescape off %}{{ email }} wants to notify you that the following package may be out-of-date:
+{% autoescape off %}{{ email }} wants to notify you that the following packages may be out-of-date:
- Package Name: {{ pkg.pkgname }}
- Architecture: {{ pkg.arch.name }}
- Repository: {{ pkg.repo.name }}
- ({{ weburl }})
+{% for p in packages %}
+* {{ p.pkgname }} {{ p.full_version }} [{{ p.repo.name|lower }}] ({{ p.arch.name }}): {{ p.get_full_url }}{% endfor %}
{% if message %}
The user provided the following additional text:
diff --git a/templates/packages/stale_relations.html b/templates/packages/stale_relations.html
index 8e2f8930..d51f7e44 100644
--- a/templates/packages/stale_relations.html
+++ b/templates/packages/stale_relations.html
@@ -17,6 +17,7 @@
<th>Packages</th>
<th>User</th>
<th>Type</th>
+ <th>Created</th>
</tr>
</thead>
<tbody>
@@ -30,6 +31,7 @@
{% endfor %}</td>
<td>{{ relation.user.get_full_name }}</td>
<td>{{ relation.get_type_display }}</td>
+ <td>{{ relation.created }}</td>
</tr>
{% empty %}
<tr class="empty"><td colspan="5"><em>No inactive user relations.</em></td></tr>
@@ -46,6 +48,7 @@
<th>Package Base</th>
<th>User</th>
<th>Type</th>
+ <th>Created</th>
</tr>
</thead>
<tbody>
@@ -55,6 +58,7 @@
<td>{{ relation.pkgbase }}</td>
<td>{{ relation.user.get_full_name }}</td>
<td>{{ relation.get_type_display }}</td>
+ <td>{{ relation.created }}</td>
</tr>
{% empty %}
<tr class="empty"><td colspan="4"><em>No non-existent pkgbase relations.</em></td></tr>
@@ -71,6 +75,7 @@
<th>Package Base</th>
<th>Packages</th>
<th>User</th>
+ <th>Created</th>
<th>Allowed Repos</th>
<th>Currently in Repos</th>
</tr>
@@ -85,6 +90,7 @@
title="View package details for {{ pkg.pkgname }}">{{ pkg.repo|lower }}/{{ pkg.pkgname }} ({{ pkg.arch }})</a>{% if not forloop.last %}, {% endif %}
{% endfor %}</td>
<td>{{ relation.user.get_full_name }}</td>
+ <td>{{ relation.created }}</td>
<td class="wrap">{{ relation.user.userprofile.allowed_repos.all|join:", " }}</td>
<td class="wrap">{{ relation.repositories|join:", " }}</td>
</tr>
diff --git a/templates/public/index.html b/templates/public/index.html
index 3432ccad..6254d7b0 100644
--- a/templates/public/index.html
+++ b/templates/public/index.html
@@ -73,15 +73,13 @@
<table>
{% for update in pkg_updates %}
- {% with update|first as fpkg %}
<tr>
- <td class="pkg-name"><span class="{{ fpkg.repo|lower }}">{{ fpkg.pkgname }} {{ fpkg.full_version }}</span></td>
- <td class="pkg-arch">
- {% for pkg in update %}<a href="{{ pkg.get_absolute_url }}"
+ <td class="pkg-name"><span class="{{ update.repo|lower }}">{{ update.pkgbase }} {{ update.version }}</span></td>
+ <td class="pkg-arch">
+ {% for pkg in update.package_links %}<a href="{{ pkg.get_absolute_url }}"
title="Details for {{ pkg.pkgname }} [{{ pkg.repo|lower }}]">{{ pkg.arch }}</a>{% if not forloop.last %}/{% endif %}{% endfor %}
</td>
</tr>
- {% endwith %}
{% endfor %}
</table>
</div>
@@ -145,14 +143,12 @@
title="View/search the package repository database">Packages</a></li>
<li><a href="/groups/"
title="View the available package groups">Package Groups</a></li>
- <li><a href="http://projects.parabolagnulinux.org"
+ <li><a href="https://projects.parabolagnulinux.org"
title="Official Parabola projects (git)">Projects in Git</a></li>
- <li><a href="http://bugs.parabolagnulinux.org/"
+ <li><a href="https://bugs.parabolagnulinux.org/"
title="Parabola's Issue Tracker">Issue Tracker</a></li>
- {% comment %}
<li><a href="/todolists/"
- title="Todo Lists">Todo Lists</a></li>
- {% endcomment %}
+ title="Hacker Todo Lists">Todo Lists</a></li>
</ul>
<h4>About</h4>
diff --git a/templates/releng/add.html b/templates/releng/add.html
new file mode 100644
index 00000000..8488b40c
--- /dev/null
+++ b/templates/releng/add.html
@@ -0,0 +1,27 @@
+{% extends "base.html" %}
+
+{% block title %}Arch Linux - Test Result Entry{% endblock %}
+
+{% block content %}
+<div class="box">
+ <h2>Arch Releng Testbuild Feedback Entry</h2>
+
+ <p>This page allows you to submit feedback after testing an Arch Linux installation
+ using a release engineering testbuild. Mark all the options you used during the
+ installation; at the end you can specify whether everything went OK. Be
+ sure to only denote a successful install after having checked the
+ installation properly. Some options require you to check several things (such as
+ config files), this will be mentioned alongside the option.</p>
+ <p>There is also an overview of all feedback on the
+ <a href="{% url releng-test-overview %}">results page</a>. Once we have
+ builds that are properly tested (enough successful feedback for all
+ important features of the ISO or a slightly earlier ISO), we can release new
+ official media.</p>
+
+ <div id="releng-feedback"> <form action="" method="post">{% csrf_token %}
+ {{ form.as_p }}
+ <input type="submit" value="Submit" />
+ </form>
+ </div>
+</div>
+{% endblock %}
diff --git a/templates/releng/result_list.html b/templates/releng/result_list.html
new file mode 100644
index 00000000..b3ae025b
--- /dev/null
+++ b/templates/releng/result_list.html
@@ -0,0 +1,41 @@
+{% extends "base.html" %}
+
+{% block content %}
+<div class="box">
+ <h2>Results for:
+ {% if option %}{{ option|title }}: {{ value }}{% endif %}
+ {{ iso_name|default:"" }}
+ </h2>
+
+ <p><a href="{% url releng-test-overview %}">Go back to testing results</a></p>
+
+ <table id="releng-result" class="results">
+ <thead>
+ <tr>
+ <th>Iso</th>
+ <th>Submitted By</th>
+ <th>Date Submitted</th>
+ <th>Success</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for test in test_list %}
+ <tr>
+ <td>{{ test.iso.name }}</td>
+ <td>{{ test.user_name }}</td>
+ <td>{{ test.created|date }}</td>
+ <td>{{ test.success|yesno }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+</div>
+{% load cdn %}{% jquery %}
+<script type="text/javascript" src="/media/jquery.tablesorter.min.js"></script>
+<script type="text/javascript" src="/media/archweb.js"></script>
+<script type="text/javascript">
+$(document).ready(function() {
+ $(".results:not(:has(tbody tr.empty))").tablesorter({widgets: ['zebra']});
+});
+</script>
+{% endblock %}
diff --git a/templates/releng/result_section.html b/templates/releng/result_section.html
new file mode 100644
index 00000000..08e46fb7
--- /dev/null
+++ b/templates/releng/result_section.html
@@ -0,0 +1,28 @@
+<tr>
+ <th>{% if option.is_rollback %}Rollback: {% endif %}{{ option.name|title }}</td>
+ <th>Last Success</th>
+ <th>Last Failure</th>
+</tr>
+{% for item in option.values %}
+<tr>
+ <td>
+ <a href="{% url releng-results-for option.name|lower item.value.pk %}">
+ {{ item.value.name|lower }}
+ </a>
+ </td>
+ <td>
+ {% if item.success %}
+ <a href="{% url releng-results-iso item.success.pk %}">
+ {{ item.success.name }}
+ </a>
+ {% else %}Never succeeded{% endif %}
+ </td>
+ <td>
+ {% if item.failure %}
+ <a href="{% url releng-results-iso item.failure.pk %}">
+ {{ item.failure.name }}
+ </a>
+ {% else %}Never failed{% endif %}
+ </td>
+</tr>
+{% endfor %}
diff --git a/templates/releng/results.html b/templates/releng/results.html
new file mode 100644
index 00000000..c3e7d99a
--- /dev/null
+++ b/templates/releng/results.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+
+{% block title %}Arch Linux - Release Engineering Testbuild Results{% endblock %}
+
+{% block content %}
+<div class="box">
+ <h2>Release Engineering Testbuild Results</h2>
+
+ <p>This is an overview screen showing a test results matrix of release
+ engineering produced ISOs. Various options and configurations are shown
+ with last success and last failure results, if known. To help improve ISO
+ quality, you are encouraged to <a href="{% url releng-test-submit %}">give feedback</a>
+ if you have tested and used any ISOs. Both successful and failed results
+ are encouraged and welcome.</p>
+
+ <p>For more information, see the <a
+ href="https://wiki.archlinux.org/index.php/DeveloperWiki:releng_testimages_feedback">documentation
+ on the wiki</a>.</p>
+
+ <p>All ISOs referenced on this page are available from
+ <a href="{{ iso_url }}">{{ iso_url }}</a>.</p>
+
+ <table>
+ {% for option in options %}
+ {% include "releng/result_section.html" %}
+ {% endfor %}
+ </table>
+</div>
+{% endblock %}
diff --git a/templates/releng/thanks.html b/templates/releng/thanks.html
new file mode 100644
index 00000000..b261426d
--- /dev/null
+++ b/templates/releng/thanks.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+
+{% block title %}Arch Linux - Feedback - Thanks!{% endblock %}
+
+{% block content %}
+<div class="box">
+ <h2>Thanks!</h2>
+ <p>Thank you for taking the time to give us this information!
+ Your results have been succesfully added to our database.</p>
+ <p>You can now <a href="{% url releng-test-overview %}">go back to the results</a>,
+ or <a href="{% url releng-test-submit %}">give more feedback</a>.</p>
+</div>
+{% endblock %}
diff --git a/templates/todolists/email_notification.txt b/templates/todolists/email_notification.txt
index 1825912c..10b50f64 100644
--- a/templates/todolists/email_notification.txt
+++ b/templates/todolists/email_notification.txt
@@ -1,7 +1,7 @@
{% autoescape off %}The todo list {{ todolist.name }} has had the following packages added to it for which you are a maintainer:
{% for tpkg in todo_packages %}
-{{ tpkg.pkg.repo.name|lower }}/{{ tpkg.pkg.pkgname }} ({{ tpkg.pkg.arch.name }}) - {{ tpkg.pkg.get_full_url }}{% endfor %}
+* {{ tpkg.pkg.repo.name|lower }}/{{ tpkg.pkg.pkgname }} ({{ tpkg.pkg.arch.name }}) - {{ tpkg.pkg.get_full_url }}{% endfor %}
Todo list information:
Creator: {{todolist.creator.get_full_name}}
diff --git a/todolists/urls.py b/todolists/urls.py
index 187d4820..2612a52e 100644
--- a/todolists/urls.py
+++ b/todolists/urls.py
@@ -1,13 +1,16 @@
from django.conf.urls.defaults import patterns
+from django.contrib.auth.decorators import permission_required
+
+from .views import DeleteTodolist
urlpatterns = patterns('todolists.views',
- (r'^$', 'list'),
+ (r'^$', 'todolist_list'),
(r'^(\d+)/$', 'view'),
(r'^add/$', 'add'),
(r'^edit/(?P<list_id>\d+)/$', 'edit'),
(r'^flag/(\d+)/(\d+)/$', 'flag'),
- (r'^delete/(?P<object_id>\d+)/$',
- 'delete_todolist'),
+ (r'^delete/(?P<pk>\d+)/$',
+ permission_required('main.delete_todolist')(DeleteTodolist.as_view())),
)
# vim: set ts=4 sw=4 et:
diff --git a/todolists/utils.py b/todolists/utils.py
new file mode 100644
index 00000000..894f3f1d
--- /dev/null
+++ b/todolists/utils.py
@@ -0,0 +1,19 @@
+from django.db.models import Count
+
+from main.models import Todolist
+
+def get_annotated_todolists():
+ qs = Todolist.objects.all()
+ lists = qs.select_related('creator').annotate(
+ pkg_count=Count('todolistpkg')).order_by('-date_added')
+ incomplete = qs.filter(todolistpkg__complete=False).annotate(
+ Count('todolistpkg')).values_list('id', 'todolistpkg__count')
+
+ # tag each list with an incomplete package count
+ lookup = dict(incomplete)
+ for todolist in lists:
+ todolist.incomplete_count = lookup.get(todolist.id, 0)
+
+ return lists
+
+# vim: set ts=4 sw=4 et:
diff --git a/todolists/views.py b/todolists/views.py
index 6278c6bf..d6a25463 100644
--- a/todolists/views.py
+++ b/todolists/views.py
@@ -5,14 +5,14 @@ from django.core.mail import send_mail
from django.shortcuts import get_object_or_404, redirect
from django.contrib.auth.decorators import login_required, permission_required
from django.db import transaction
-from django.db.models import Count
from django.views.decorators.cache import never_cache
-from django.views.generic.create_update import delete_object
+from django.views.generic import DeleteView
from django.views.generic.simple import direct_to_template
from django.template import Context, loader
from django.utils import simplejson
from main.models import Todolist, TodolistPkg, Package
+from .utils import get_annotated_todolists
class TodoListForm(forms.ModelForm):
packages = forms.CharField(required=False,
@@ -35,7 +35,7 @@ class TodoListForm(forms.ModelForm):
@permission_required('main.change_todolistpkg')
@never_cache
def flag(request, listid, pkgid):
- list = get_object_or_404(Todolist, id=listid)
+ todolist = get_object_or_404(Todolist, id=listid)
pkg = get_object_or_404(TodolistPkg, id=pkgid)
pkg.complete = not pkg.complete
pkg.save()
@@ -43,29 +43,18 @@ def flag(request, listid, pkgid):
return HttpResponse(
simplejson.dumps({'complete': pkg.complete}),
mimetype='application/json')
- return redirect(list)
+ return redirect(todolist)
@login_required
@never_cache
def view(request, listid):
- list = get_object_or_404(Todolist, id=listid)
- return direct_to_template(request, 'todolists/view.html', {'list': list})
+ todolist = get_object_or_404(Todolist, id=listid)
+ return direct_to_template(request, 'todolists/view.html', {'list': todolist})
@login_required
@never_cache
-def list(request):
- lists = Todolist.objects.select_related('creator').annotate(
- pkg_count=Count('todolistpkg')).order_by('-date_added')
- incomplete = Todolist.objects.filter(todolistpkg__complete=False).annotate(
- Count('todolistpkg')).values_list('id', 'todolistpkg__count')
-
- # tag each list with an incomplete package count
- lookup = {}
- for k, v in incomplete:
- lookup[k] = v
- for l in lists:
- l.incomplete_count = lookup.get(l.id, 0)
-
+def todolist_list(request):
+ lists = get_annotated_todolists()
return direct_to_template(request, 'todolists/list.html', {'lists': lists})
@permission_required('main.add_todolist')
@@ -109,13 +98,11 @@ def edit(request, list_id):
}
return direct_to_template(request, 'general_form.html', page_dict)
-@permission_required('main.delete_todolist')
-@never_cache
-def delete_todolist(request, object_id):
- return delete_object(request, object_id=object_id, model=Todolist,
- template_name="todolists/todolist_confirm_delete.html",
- post_delete_redirect='/todo/')
-
+class DeleteTodolist(DeleteView):
+ model = Todolist
+ # model in main == assumes name 'main/todolist_confirm_delete.html'
+ template_name = 'todolists/todolist_confirm_delete.html'
+ success_url = '/todo/'
@transaction.commit_on_success
def create_todolist_packages(form, creator=None):
@@ -163,13 +150,13 @@ def send_todolist_emails(todo_list, new_packages):
maint_packages.setdefault(maint, []).append(todo_package)
for maint, packages in maint_packages.iteritems():
- c = Context({
+ ctx = Context({
'todo_packages': sorted(packages),
'todolist': todo_list,
})
- t = loader.get_template('todolists/email_notification.txt')
+ template = loader.get_template('todolists/email_notification.txt')
send_mail('Packages added to todo list \'%s\'' % todo_list.name,
- t.render(c),
+ template.render(ctx),
'Parabola <packages@list.parabolagnulinux.org>',
[maint],
fail_silently=True)
diff --git a/urls.py b/urls.py
index 05f11c15..8cae8660 100644
--- a/urls.py
+++ b/urls.py
@@ -4,7 +4,7 @@ from django.conf.urls.defaults import *
from django.conf import settings
from django.contrib import admin
-from django.views.generic.simple import direct_to_template
+from django.views.generic import TemplateView
from feeds import PackageFeed, NewsFeed
import sitemaps
@@ -49,12 +49,15 @@ urlpatterns += patterns('django.contrib.auth.views',
# Public pages
urlpatterns += patterns('public.views',
(r'^$', 'index', {}, 'index'),
- (r'^about/$', direct_to_template, {'template': 'public/about.html'}, 'page-about'),
- (r'^art/$', direct_to_template, {'template': 'public/art.html'}, 'page-art'),
- (r'^svn/$', direct_to_template, {'template': 'public/svn.html'}, 'page-svn'),
- (r'^hackers/$', 'userlist', { 'type':'hackers' }, 'page-devs'),
+ (r'^about/$', TemplateView.as_view(template_name='public/about.html'),
+ {}, 'page-about'),
+ (r'^art/$', TemplateView.as_view(template_name='public/art.html'),
+ {}, 'page-art'),
+ (r'^svn/$', TemplateView.as_view(template_name='public/svn.html'),
+ {}, 'page-svn'),
+ (r'^hackers/$', 'userlist', { 'type':'hackers' }, 'page-devs'),
(r'^fellows/$', 'userlist', { 'type':'fellows' }, 'page-fellows'),
- (r'^donate/$', 'donate', {}, 'page-donate'),
+ (r'^donate/$', 'donate', {}, 'page-donate'),
(r'^download/$', 'download', {}, 'page-download'),
)
@@ -69,6 +72,7 @@ urlpatterns += patterns('',
(r'^mirrors/', include('mirrors.urls')),
(r'^news/', include('news.urls')),
(r'^packages/', include('packages.urls')),
+ (r'^releng/', include('releng.urls')),
(r'^todo/', include('todolists.urls')),
(r'^opensearch/packages/$', 'packages.views.opensearch',
{}, 'opensearch-packages'),
@@ -77,7 +81,7 @@ urlpatterns += patterns('',
if settings.DEBUG == True:
urlpatterns += patterns('',
- (r'^media/(.*)$', 'django.views.static.serve',
+ (r'^media/(.*)$', 'django.views.static.serve',
{'document_root': os.path.join(settings.DEPLOY_PATH, 'media')}))
# vim: set ts=4 sw=4 et: