summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorParabola <dev@list.parabolagnulinux.org>2011-05-21 05:41:26 +0000
committerParabola <dev@list.parabolagnulinux.org>2011-05-21 05:41:26 +0000
commit62059d83aea71ac7bde8902b20221e52c86a810b (patch)
tree758bd028d43a30893ba6261f39b1ebdbebbbf6e3
parent081223981aa520f792757a1776588756a4107fd4 (diff)
parentfd9ecb6eb1c8ee56adfbb58640d7a98baa6cd62c (diff)
Merge branch 'master' of /srv/git/projects/parabolaweb
-rw-r--r--README9
-rw-r--r--devel/management/commands/reporead.py176
-rw-r--r--devel/urls.py8
-rw-r--r--devel/views.py189
-rw-r--r--feeds.py99
-rw-r--r--main/admin.py16
-rw-r--r--main/fixtures/groups.json218
-rw-r--r--main/fixtures/repos.json56
-rw-r--r--main/migrations/0043_auto__add_field_package_epoch.py162
-rw-r--r--main/migrations/0044_auto__chg_field_todolist_date_added.py156
-rw-r--r--main/migrations/0045_add_todolist_date_added_index.py156
-rw-r--r--main/migrations/0046_auto__add_field_repo_staging.py157
-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.py70
-rw-r--r--main/templatetags/attributes.py21
-rw-r--r--main/utils.py34
-rw-r--r--media/archweb.css19
-rw-r--r--media/archweb.js44
-rw-r--r--media/django-jsi18n.js35
-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/0006_auto__add_provision__add_conflict__add_replacement.py167
-rw-r--r--packages/migrations/0007_auto__add_field_packagerelation_created.py135
-rw-r--r--packages/models.py59
-rw-r--r--packages/templatetags/package_extras.py35
-rw-r--r--packages/urls.py5
-rw-r--r--packages/utils.py2
-rw-r--r--packages/views.py154
-rw-r--r--public/utils.py80
-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.json72
-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.py137
-rw-r--r--requirements.txt4
-rw-r--r--requirements_prod.txt6
-rw-r--r--settings.py44
-rw-r--r--templates/admin/index.html8
-rw-r--r--templates/devel/admin_log.html62
-rw-r--r--templates/devel/clock.html4
-rw-r--r--templates/devel/index.html100
-rw-r--r--templates/devel/packages.html62
-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/news/add.html8
-rw-r--r--templates/packages/details.html18
-rw-r--r--templates/packages/differences.html4
-rw-r--r--templates/packages/files.html4
-rw-r--r--templates/packages/flag.html32
-rw-r--r--templates/packages/flag_confirmed.html19
-rw-r--r--templates/packages/flaghelp.html7
-rw-r--r--templates/packages/opensearch.xml2
-rw-r--r--templates/packages/outofdate.txt8
-rw-r--r--templates/packages/packages_list.html (renamed from templates/packages/group_details.html)10
-rw-r--r--templates/packages/search.html89
-rw-r--r--templates/packages/signoffs.html2
-rw-r--r--templates/packages/stale_relations.html8
-rw-r--r--templates/public/feeds.html9
-rw-r--r--templates/public/index.html16
-rw-r--r--templates/public/svn.html33
-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.html25
-rw-r--r--templates/releng/thanks.html13
-rw-r--r--templates/todolists/email_notification.txt2
-rw-r--r--templates/todolists/list.html4
-rw-r--r--templates/todolists/public_list.html11
-rw-r--r--templates/todolists/view.html4
-rw-r--r--todolists/urls.py9
-rw-r--r--todolists/utils.py19
-rw-r--r--todolists/views.py47
-rw-r--r--urls.py18
107 files changed, 4359 insertions, 693 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 09e48559..a8875c7e 100644
--- a/devel/management/commands/reporead.py
+++ b/devel/management/commands/reporead.py
@@ -35,12 +35,11 @@ try:
except ImportError:
pass
-from logging import ERROR, WARNING, INFO, DEBUG
-
from main.models import Arch, Package, PackageDepend, PackageFile, Repo
+from packages.models import Conflict, Provision, Replacement
logging.basicConfig(
- level=WARNING,
+ level=logging.WARNING,
format='%(asctime)s -> %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
stream=sys.stderr)
@@ -69,11 +68,11 @@ class Command(BaseCommand):
v = int(options.get('verbosity', 0))
if v == 0:
- logger.level = ERROR
+ logger.level = logging.ERROR
elif v == 1:
- logger.level = INFO
+ logger.level = logging.INFO
elif v == 2:
- logger.level = DEBUG
+ logger.level = logging.DEBUG
import signal, traceback
handler = lambda sig, stack: traceback.print_stack(stack)
@@ -88,13 +87,23 @@ class Pkg(object):
bare = ( 'name', 'base', 'arch', 'desc', 'filename',
'md5sum', 'url', 'builddate', 'packager' )
number = ( 'csize', 'isize' )
+ collections = ( 'depends', 'optdepends', 'conflicts',
+ 'provides', 'replaces', 'groups', 'license', 'files' )
+
+ version_re = re.compile(r'^((\d+):)?(.+)-([^-]+)$')
def __init__(self, repo):
self.repo = repo
self.ver = None
self.rel = None
+ self.epoch = 0
for k in self.bare + self.number:
setattr(self, k, None)
+ for k in self.collections:
+ setattr(self, k, ())
+ # So we can tell the diffence between a package with no files, and a DB
+ # without files entries
+ self.has_files = False
def populate(self, values):
for k, v in values.iteritems():
@@ -103,16 +112,26 @@ class Pkg(object):
setattr(self, k, v[0][:254])
elif k in self.number:
setattr(self, k, long(v[0]))
- elif k == 'force':
- setattr(self, k, True)
elif k == 'version':
- ver, rel = v[0].rsplit('-')
- setattr(self, 'ver', ver)
- setattr(self, 'rel', rel)
+ match = self.version_re.match(v[0])
+ self.ver = match.group(3)
+ self.rel = match.group(4)
+ if match.group(2):
+ self.epoch = int(match.group(2))
+ elif k == 'files':
+ self.files = v
+ self.has_files = True
else:
- # files, depends, etc.
+ # anything left in collections
setattr(self, k, v)
+ @property
+ def full_version(self):
+ '''Very similar to the main.models.Package method.'''
+ if self.epoch > 0:
+ return u'%d:%s-%s' % (self.epoch, self.ver, self.rel)
+ return u'%s-%s' % (self.ver, self.rel)
+
def find_user(userstring):
'''
@@ -163,20 +182,58 @@ def find_user(userstring):
# lookup more than strictly necessary.
find_user.cache = {}
+DEPEND_RE = re.compile(r"^(.+?)((>=|<=|=|>|<)(.*))?$")
+
def create_depend(package, dep_str, optional=False):
depend = PackageDepend(pkg=package, optional=optional)
# lop off any description first
parts = dep_str.split(':', 1)
if len(parts) > 1:
depend.description = parts[1].strip()
- match = re.match(r"^(.+?)((>=|<=|=|>|<)(.*))?$", parts[0].strip())
+ match = DEPEND_RE.match(parts[0].strip())
if match:
depend.depname = match.group(1)
if match.group(2):
depend.depvcmp = match.group(2)
+ else:
+ logger.warning('Package %s had unparsable depend string %s',
+ package.pkgname, dep_str)
+ return None
depend.save(force_insert=True)
return depend
+def create_related(model, package, rel_str, equals_only=False):
+ related = model(pkg=package)
+ match = DEPEND_RE.match(rel_str)
+ if match:
+ related.name = match.group(1)
+ if match.group(3):
+ comp = match.group(3)
+ if not equals_only:
+ related.comparison = comp
+ elif comp != '=':
+ logger.warning(
+ 'Package %s had unexpected comparison operator %s for %s in %s',
+ package.pkgname, comp, model.__name__, rel_str)
+ if match.group(4):
+ related.version = match.group(4)
+ else:
+ logger.warning('Package %s had unparsable %s string %s',
+ package.pkgname, model.___name__, rel_str)
+ return None
+ related.save(force_insert=True)
+ return related
+
+def create_multivalued(dbpkg, repopkg, db_attr, repo_attr):
+ '''Populate the simplest of multivalued attributes. These are those that
+ only deal with a 'name' attribute, such as licenses, groups, etc. The input
+ and output objects and attribute names are specified, and everything is
+ done via getattr().'''
+ collection = getattr(dbpkg, db_attr)
+ collection.all().delete()
+ for name in getattr(repopkg, repo_attr):
+ collection.create(name=name)
+
def populate_pkg(dbpkg, repopkg, force=False, timestamp=None):
if repopkg.base:
dbpkg.pkgbase = repopkg.base
@@ -184,6 +241,7 @@ def populate_pkg(dbpkg, repopkg, force=False, timestamp=None):
dbpkg.pkgbase = repopkg.name
dbpkg.pkgver = repopkg.ver
dbpkg.pkgrel = repopkg.rel
+ dbpkg.epoch = repopkg.epoch
dbpkg.pkgdesc = repopkg.desc
dbpkg.url = repopkg.url
dbpkg.filename = repopkg.filename
@@ -210,38 +268,39 @@ def populate_pkg(dbpkg, repopkg, force=False, timestamp=None):
populate_files(dbpkg, repopkg, force=force)
dbpkg.packagedepend_set.all().delete()
- if hasattr(repopkg, 'depends'):
- for y in repopkg.depends:
- dep = create_depend(dbpkg, y)
- if hasattr(repopkg, 'optdepends'):
- for y in repopkg.optdepends:
- dep = create_depend(dbpkg, y, True)
+ for y in repopkg.depends:
+ create_depend(dbpkg, y)
+ for y in repopkg.optdepends:
+ create_depend(dbpkg, y, True)
- dbpkg.groups.all().delete()
- if hasattr(repopkg, 'groups'):
- for y in repopkg.groups:
- dbpkg.groups.create(name=y)
+ dbpkg.conflicts.all().delete()
+ for y in repopkg.conflicts:
+ create_related(Conflict, dbpkg, y)
+ dbpkg.provides.all().delete()
+ for y in repopkg.provides:
+ create_related(Provision, dbpkg, y, equals_only=True)
+ dbpkg.replaces.all().delete()
+ for y in repopkg.replaces:
+ create_related(Replacement, dbpkg, y)
- dbpkg.licenses.all().delete()
- if hasattr(repopkg, 'license'):
- for y in repopkg.license:
- dbpkg.licenses.create(name=y)
+ create_multivalued(dbpkg, repopkg, 'groups', 'groups')
+ create_multivalued(dbpkg, repopkg, 'licenses', 'license')
def populate_files(dbpkg, repopkg, force=False):
if not force:
- if dbpkg.pkgver != repopkg.ver or dbpkg.pkgrel != repopkg.rel:
- logger.info("db version (%s-%s) didn't match repo version (%s-%s) "
- "for package %s, skipping file list addition",
- dbpkg.pkgver, dbpkg.pkgrel, repopkg.ver, repopkg.rel,
- dbpkg.pkgname)
+ if dbpkg.pkgver != repopkg.ver or dbpkg.pkgrel != repopkg.rel \
+ or dbpkg.epoch != repopkg.epoch:
+ logger.info("DB version (%s) didn't match repo version "
+ "(%s) for package %s, skipping file list addition",
+ dbpkg.full_version, repopkg.full_version, dbpkg.pkgname)
return
if not dbpkg.files_last_update or not dbpkg.last_update:
pass
elif dbpkg.files_last_update > dbpkg.last_update:
return
# only delete files if we are reading a DB that contains them
- if hasattr(repopkg, 'files'):
+ if repopkg.has_files:
dbpkg.packagefile_set.all().delete()
logger.info("adding %d files for package %s",
len(repopkg.files), dbpkg.pkgname)
@@ -255,8 +314,8 @@ def populate_files(dbpkg, repopkg, force=False):
is_directory=(filename is None),
directory=dirname + '/',
filename=filename)
- pkgfile.save()
- dbpkg.files_last_update = datetime.now()
+ pkgfile.save(force_insert=True)
+ dbpkg.files_last_update = datetime.utcnow()
dbpkg.save()
@transaction.commit_on_success
@@ -273,25 +332,23 @@ def db_update(archname, reponame, pkgs, options):
filesonly = options.get('filesonly', False)
repository = Repo.objects.get(name__iexact=reponame)
architecture = Arch.objects.get(name__iexact=archname)
- dbpkgs = Package.objects.filter(arch=architecture, repo=repository)
- # It makes sense to fully evaluate our DB query now because we will
- # be using 99% of the objects in our "in both sets" loop. Force eval
- # by calling list() on the QuerySet.
- list(dbpkgs)
+ # no-arg order_by() removes even the default ordering; we don't need it
+ dbpkgs = Package.objects.filter(
+ arch=architecture, repo=repository).order_by()
# This makes our inner loop where we find packages by name *way* more
# efficient by not having to go to the database for each package to
# SELECT them by name.
dbdict = dict([(pkg.pkgname, pkg) for pkg in dbpkgs])
logger.debug("Creating sets")
- dbset = set([pkg.pkgname for pkg in dbpkgs])
+ dbset = set(dbdict.keys())
syncset = set([pkg.name for pkg in pkgs])
logger.info("%d packages in current web DB", len(dbset))
logger.info("%d packages in new updating db", len(syncset))
in_sync_not_db = syncset - dbset
logger.info("%d packages in sync not db", len(in_sync_not_db))
- # Try to catch those random orphaning issues that make Eric so unhappy.
+ # Try to catch those random package deletions that make Eric so unhappy.
if len(dbset):
dbpercent = 100.0 * len(syncset) / len(dbset)
else:
@@ -302,12 +359,14 @@ def db_update(archname, reponame, pkgs, options):
# means we expect the repo to fluctuate a lot.
msg = "Package database has %.1f%% the number of packages in the " \
"web database" % dbpercent
- if not filesonly and \
+ if len(dbset) == 0 and len(syncset) == 0:
+ pass
+ elif not filesonly and \
len(dbset) > 20 and dbpercent < 50.0 and \
- not repository.testing:
+ not repository.testing and not repository.staging:
logger.error(msg)
raise Exception(msg)
- if dbpercent < 75.0:
+ elif dbpercent < 75.0:
logger.warning(msg)
if not filesonly:
@@ -315,14 +374,14 @@ 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
for p in in_db_not_sync:
logger.info("Removing package %s from database", p)
- Package.objects.get(
- pkgname=p, arch=architecture, repo=repository).delete()
+ dbp = dbdict[p]
+ dbp.delete()
# packages in both database and in syncdb (update in database)
pkg_in_both = syncset & dbset
@@ -334,11 +393,12 @@ def db_update(archname, reponame, pkgs, options):
# for a non-force, we don't want to do anything at all.
if filesonly:
pass
- elif p.ver == dbp.pkgver and p.rel == dbp.pkgrel:
+ elif p.ver == dbp.pkgver and p.rel == dbp.pkgrel \
+ and p.epoch == dbp.epoch:
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)
@@ -421,10 +481,9 @@ def parse_repo(repopath):
logger.info("Finished repo parsing, %d total packages", len(pkgs))
return (reponame, pkgs.values())
-def validate_arch(arch):
+def validate_arch(archname):
"Check if arch is valid."
- available_arches = [x.name for x in Arch.objects.all()]
- return arch in available_arches
+ return Arch.objects.filter(name__iexact=archname).exists()
def read_repo(primary_arch, repo_file, options):
"""
@@ -432,21 +491,22 @@ def read_repo(primary_arch, repo_file, options):
"""
repo, packages = parse_repo(repo_file)
- # sort packages by arch -- to handle noarch stuff
+ # group packages by arch -- to handle noarch stuff
packages_arches = {}
- packages_arches['any'] = []
+ for arch in Arch.objects.filter(agnostic=True):
+ packages_arches[arch.name] = []
packages_arches[primary_arch] = []
for package in packages:
- if package.arch in ('any', primary_arch):
+ if package.arch in packages_arches:
packages_arches[package.arch].append(package)
else:
# we don't include mis-arched packages
logger.warning("Package %s arch = %s",
package.name,package.arch)
logger.info('Starting database updates.')
- for (arch, pkgs) in packages_arches.items():
- db_update(arch, repo, pkgs, options)
+ for arch in sorted(packages_arches.keys()):
+ db_update(arch, repo, packages_arches[arch], options)
logger.info('Finished database updates.')
return 0
diff --git a/devel/urls.py b/devel/urls.py
index bcf9c071..8759562e 100644
--- a/devel/urls.py
+++ b/devel/urls.py
@@ -1,10 +1,14 @@
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'^profile/$', 'change_profile'),
+ (r'^reports/(?P<report>.*)/(?P<username>.*)/$', 'report'),
+ (r'^reports/(?P<report>.*)/$', 'report'),
)
# vim: set ts=4 sw=4 et:
diff --git a/devel/views.py b/devel/views.py
index 311922ca..c0d10285 100644
--- a/devel/views.py
+++ b/devel/views.py
@@ -1,20 +1,28 @@
from django import forms
from django.http import HttpResponseRedirect
-from django.contrib.auth.decorators import login_required, permission_required
-from django.contrib.auth.models import User
+from django.contrib.auth.decorators import \
+ login_required, permission_required, user_passes_test
+from django.contrib.auth.models import User, Group
from django.contrib.sites.models import Site
from django.core.mail import send_mail
+from django.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
@@ -22,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')
@@ -32,13 +40,27 @@ 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(
+ type=PackageRelation.MAINTAINER).values('pkgbase')
+ total_orphans = Package.objects.exclude(pkgbase__in=maintained).count()
+ total_flagged_orphans = Package.objects.filter(
+ flag_date__isnull=False).exclude(pkgbase__in=maintained).count()
+ orphan = {
+ 'package_count': total_orphans,
+ 'flagged_count': total_flagged_orphans,
+ }
+
page_dict = {
- 'todos': Todolist.objects.incomplete().order_by('-date_added'),
+ 'todos': todolists,
'repos': Repo.objects.all(),
'arches': Arch.objects.all(),
'maintainers': maintainers,
+ 'orphan': orphan,
'flagged' : flagged,
'todopkgs' : todopkgs,
}
@@ -52,10 +74,12 @@ 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:
- tz = pytz.timezone(dev.userprofile.time_zone)
+ # Work around https://bugs.launchpad.net/pytz/+bug/718673
+ timezone = str(dev.userprofile.time_zone)
+ tz = pytz.timezone(timezone)
dev.current_time = utc_now.astimezone(tz)
page_dict = {
@@ -103,14 +127,98 @@ def change_profile(request):
return direct_to_template(request, 'devel/profile.html',
{'form': form, 'profile_form': profile_form})
+@login_required
+def report(request, report, username=None):
+ title = 'Developer Report'
+ packages = Package.objects.select_related('arch', 'repo')
+ names = attrs = user = 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
+
+ if username:
+ user = get_object_or_404(User, username=username, is_active=True)
+ maintained = PackageRelation.objects.filter(user=user,
+ type=PackageRelation.MAINTAINER).values('pkgbase')
+ packages = packages.filter(pkgbase__in=maintained)
+
+ maints = User.objects.filter(id__in=PackageRelation.objects.filter(
+ type=PackageRelation.MAINTAINER).values('user'))
+
+ context = {
+ 'all_maintainers': maints,
+ 'title': title,
+ 'maintainer': user,
+ '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']
@@ -119,38 +227,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),
- 'Arch Website Notification <nobody@archlinux.org>',
+ 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:
@@ -167,4 +298,16 @@ def new_user_form(request):
}
return direct_to_template(request, 'general_form.html', context)
+@user_passes_test(lambda u: u.is_superuser)
+@never_cache
+def admin_log(request, username=None):
+ user = None
+ if username:
+ user = get_object_or_404(User, username=username)
+ context = {
+ 'title': "Admin Action Log",
+ 'log_user': user,
+ }
+ return direct_to_template(request, 'devel/admin_log.html', context)
+
# vim: set ts=4 sw=4 et:
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 51859ad3..e86e5cab 100644
--- a/main/admin.py
+++ b/main/admin.py
@@ -1,7 +1,7 @@
from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin
-from main.models import Arch, Donor, Package, Repo, UserProfile
+from main.models import Arch, Donor, Package, Repo, Todolist, UserProfile
class DonorAdmin(admin.ModelAdmin):
list_display = ('name', 'visible')
@@ -14,15 +14,21 @@ class ArchAdmin(admin.ModelAdmin):
search_fields = ('name',)
class RepoAdmin(admin.ModelAdmin):
- list_display = ('name', 'testing', 'bugs_project', 'svn_root')
- list_filter = ('testing',)
+ 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',)
+class TodolistAdmin(admin.ModelAdmin):
+ list_display = ('name', 'date_added', 'creator', 'description')
+ search_fields = ('name', 'description')
+
admin.site.unregister(User)
class UserProfileInline(admin.StackedInline):
model = UserProfile
@@ -40,4 +46,6 @@ admin.site.register(Package, PackageAdmin)
admin.site.register(Arch, ArchAdmin)
admin.site.register(Repo, RepoAdmin)
+admin.site.register(Todolist, TodolistAdmin)
+
# vim: set ts=4 sw=4 et:
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/0043_auto__add_field_package_epoch.py b/main/migrations/0043_auto__add_field_package_epoch.py
new file mode 100644
index 00000000..77cd9b49
--- /dev/null
+++ b/main/migrations/0043_auto__add_field_package_epoch.py
@@ -0,0 +1,162 @@
+# 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 'Package.epoch'
+ db.add_column('packages', 'epoch', self.gf('django.db.models.fields.PositiveIntegerField')(default=0), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Package.epoch'
+ db.delete_column('packages', 'epoch')
+
+
+ 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'}),
+ '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.DateField', [], {'auto_now_add': 'True', 'blank': '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/0044_auto__chg_field_todolist_date_added.py b/main/migrations/0044_auto__chg_field_todolist_date_added.py
new file mode 100644
index 00000000..d4099891
--- /dev/null
+++ b/main/migrations/0044_auto__chg_field_todolist_date_added.py
@@ -0,0 +1,156 @@
+# 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.alter_column('todolists', 'date_added', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True))
+
+ def backwards(self, orm):
+ db.alter_column('todolists', 'date_added', self.gf('django.db.models.fields.DateField')(auto_now_add=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'}),
+ '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', [], {'auto_now_add': 'True', 'blank': '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/0045_add_todolist_date_added_index.py b/main/migrations/0045_add_todolist_date_added_index.py
new file mode 100644
index 00000000..985f1c2d
--- /dev/null
+++ b/main/migrations/0045_add_todolist_date_added_index.py
@@ -0,0 +1,156 @@
+# 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_index('todolists', ['date_added'])
+
+ def backwards(self, orm):
+ db.delete_index('todolists', ['date_added'])
+
+ 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'}),
+ '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', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': '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/0046_auto__add_field_repo_staging.py b/main/migrations/0046_auto__add_field_repo_staging.py
new file mode 100644
index 00000000..40c3cb20
--- /dev/null
+++ b/main/migrations/0046_auto__add_field_repo_staging.py
@@ -0,0 +1,157 @@
+# 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', 'staging', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
+
+ def backwards(self, orm):
+ db.delete_column('repos', 'staging')
+
+ 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', [], {'auto_now_add': 'True', 'blank': '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/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 a7cc2335..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
@@ -84,8 +85,12 @@ class Repo(models.Model):
name = models.CharField(max_length=255, unique=True)
testing = models.BooleanField(default=False,
help_text="Is this repo meant for package testing?")
+ staging = models.BooleanField(default=False,
+ 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.")
@@ -101,12 +106,15 @@ 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)
pkgrel = models.CharField(max_length=255)
+ epoch = models.PositiveIntegerField(default=0)
pkgdesc = models.CharField(max_length=255, null=True)
url = models.CharField(max_length=255, null=True)
filename = models.CharField(max_length=255)
@@ -117,19 +125,25 @@ 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
+ @property
+ def full_version(self):
+ if self.epoch > 0:
+ return u'%d:%s-%s' % (self.epoch, self.pkgver, self.pkgrel)
+ return u'%s-%s' % (self.pkgver, self.pkgrel)
+
def get_absolute_url(self):
return '/packages/%s/%s/%s/' % (self.repo.name.lower(),
self.arch.name, self.pkgname)
@@ -174,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
@@ -184,7 +201,7 @@ class Package(models.Model):
# find another package by this name in the opposite testing setup
if not Package.objects.filter(pkgname=self.pkgname,
- arch=self.arch).exclude(id=self.id,
+ arch=self.arch).exclude(id=self.id).exclude(
repo__testing=self.repo.testing).exists():
# there isn't one? short circuit, all required by entries are fine
return requiredby
@@ -267,26 +284,12 @@ 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 \
and self.pkgver == other.pkgver \
- and self.pkgrel == other.pkgrel
+ and self.pkgrel == other.pkgrel \
+ and self.epoch == other.epoch
def in_testing(self):
'''attempt to locate this package in a testing repo; if we are in
@@ -338,11 +341,12 @@ 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.DateField(auto_now_add=True)
+ date_added = models.DateTimeField(db_index=True)
objects = TodolistManager()
+
def __unicode__(self):
return self.name
@@ -372,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 6ae720c1..ee4fd169 100644
--- a/media/archweb.css
+++ b/media/archweb.css
@@ -242,24 +242,17 @@ form#dash-pkg-notify input { vertical-align: middle; margin: 0 0.25em; }
form#dash-pkg-notify input[type=submit] { margin-top: -0.25em; }
form#dash-pkg-notify p { margin: 0; }
-/* dev dashboard: collapse stat tables by default */
-table#stats-by-maintainer, table#stats-by-repo, table#stats-by-arch { display: none; }
table.dash-stats .key { width: 50%; }
-span.dash-click { font-weight: normal; font-size: 0.8em; color: #888; }
-div.dash-stats h3 { color: #07b; }
-
-/* read only (public) todo lists */
-#public_todo_lists .todo_list {
- margin-left: 2em;
-}
/* dev dashboard: admin actions (add news items, todo list, etc) */
ul.admin-actions { float: right; list-style: none; margin-top: -2.5em; }
ul.admin-actions li { display: inline; padding-left: 1.5em; }
-/* dev: todo list */
+/* todo lists (public and private) */
.todo-table .complete { color: green; }
.todo-table .incomplete { color: red; }
+.todo-info { margin: 0; color: #999; }
+.todo-list h4 { margin-top: 0; margin-bottom: 0.4em; }
/* dev: signoff page */
#dev-signoffs ul { list-style: none; margin: 0; padding: 0; }
@@ -267,6 +260,12 @@ 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; }
+#releng-feedback ul+.helptext { position: relative; top: -0.9em; }
+
/* 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 52e817a4..49f2a319 100644
--- a/media/archweb.js
+++ b/media/archweb.js
@@ -67,20 +67,52 @@ 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 */
function enablePreview() {
- $('#previewbtn').click(function(event) {
+ $('#news-preview-button').click(function(event) {
event.preventDefault();
- $.post('/news/preview/',
- { data: $('#id_content').val() },
+ $.post('/news/preview/', {
+ data: $('#id_content').val(),
+ csrfmiddlewaretoken: $('#newsform input[name=csrfmiddlewaretoken]').val()
+ },
function(data) {
- $('#previewdata').html(data);
- $('.news-article').show();
+ $('#news-preview-data').html(data);
+ $('#news-preview').show();
}
);
- $('#previewtitle').html($('#id_title').val());
+ $('#news-preview-title').html($('#id_title').val());
});
}
diff --git a/media/django-jsi18n.js b/media/django-jsi18n.js
deleted file mode 100644
index 83562c1a..00000000
--- a/media/django-jsi18n.js
+++ /dev/null
@@ -1,35 +0,0 @@
-
-/* gettext library */
-
-var catalog = new Array();
-
-function pluralidx(count) { return (count == 1) ? 0 : 1; }
-
-
-function gettext(msgid) {
- var value = catalog[msgid];
- if (typeof(value) == 'undefined') {
- return msgid;
- } else {
- return (typeof(value) == 'string') ? value : value[0];
- }
-}
-
-function ngettext(singular, plural, count) {
- value = catalog[singular];
- if (typeof(value) == 'undefined') {
- return (count == 1) ? singular : plural;
- } else {
- return value[pluralidx(count)];
- }
-}
-
-function gettext_noop(msgid) { return msgid; }
-
-function interpolate(fmt, obj, named) {
- if (named) {
- return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
- } else {
- return fmt.replace(/%s/g, function(match){return String(obj.shift())});
- }
-}
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/0006_auto__add_provision__add_conflict__add_replacement.py b/packages/migrations/0006_auto__add_provision__add_conflict__add_replacement.py
new file mode 100644
index 00000000..c764ce77
--- /dev/null
+++ b/packages/migrations/0006_auto__add_provision__add_conflict__add_replacement.py
@@ -0,0 +1,167 @@
+# 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 'Provision'
+ db.create_table('packages_provision', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('pkg', self.gf('django.db.models.fields.related.ForeignKey')(related_name='provides', to=orm['main.Package'])),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+ ('version', self.gf('django.db.models.fields.CharField')(default='', max_length=255)),
+ ))
+ db.send_create_signal('packages', ['Provision'])
+
+ # Adding model 'Conflict'
+ db.create_table('packages_conflict', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('pkg', self.gf('django.db.models.fields.related.ForeignKey')(related_name='conflicts', to=orm['main.Package'])),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+ ('comparison', self.gf('django.db.models.fields.CharField')(default='', max_length=255)),
+ ('version', self.gf('django.db.models.fields.CharField')(default='', max_length=255)),
+ ))
+ db.send_create_signal('packages', ['Conflict'])
+
+ # Adding model 'Replacement'
+ db.create_table('packages_replacement', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('pkg', self.gf('django.db.models.fields.related.ForeignKey')(related_name='replaces', to=orm['main.Package'])),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+ ('comparison', self.gf('django.db.models.fields.CharField')(default='', max_length=255)),
+ ('version', self.gf('django.db.models.fields.CharField')(default='', max_length=255)),
+ ))
+ db.send_create_signal('packages', ['Replacement'])
+
+
+ def backwards(self, orm):
+ # Deleting model 'Provision'
+ db.delete_table('packages_provision')
+ # Deleting model 'Conflict'
+ db.delete_table('packages_conflict')
+ # Deleting model 'Replacement'
+ db.delete_table('packages_replacement')
+
+
+ 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_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'},
+ '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/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 5dbdea45..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
@@ -45,7 +48,7 @@ class PackageGroup(models.Model):
name = models.CharField(max_length=255)
def __unicode__(self):
- return "%s: %s" % (name, pkg)
+ return "%s: %s" % (self.name, self.pkg)
class License(models.Model):
pkg = models.ForeignKey('main.Package', related_name='licenses')
@@ -57,6 +60,50 @@ class License(models.Model):
class Meta:
ordering = ['name']
+class Conflict(models.Model):
+ pkg = models.ForeignKey('main.Package', related_name='conflicts')
+ name = models.CharField(max_length=255, db_index=True)
+ comparison = models.CharField(max_length=255, default='')
+ version = models.CharField(max_length=255, default='')
+
+ def __unicode__(self):
+ if self.version:
+ return u'%s%s%s' % (self.name, self.comparison, self.version)
+ return self.name
+
+ class Meta:
+ ordering = ['name']
+
+class Provision(models.Model):
+ pkg = models.ForeignKey('main.Package', related_name='provides')
+ name = models.CharField(max_length=255, db_index=True)
+ # comparison must be '=' for provides
+ comparison = '='
+ version = models.CharField(max_length=255, default='')
+
+ def __unicode__(self):
+ if self.version:
+ return u'%s=%s' % (self.name, self.version)
+ return self.name
+
+ class Meta:
+ ordering = ['name']
+
+class Replacement(models.Model):
+ pkg = models.ForeignKey('main.Package', related_name='replaces')
+ name = models.CharField(max_length=255, db_index=True)
+ comparison = models.CharField(max_length=255, default='')
+ version = models.CharField(max_length=255, default='')
+
+ def __unicode__(self):
+ if self.version:
+ return u'%s%s%s' % (self.name, self.comparison, self.version)
+ return self.name
+
+ class Meta:
+ ordering = ['name']
+
+
def remove_inactive_maintainers(sender, instance, created, **kwargs):
# instance is an auth.models.User; we want to remove any existing
# maintainer relations if the user is no longer active
@@ -65,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 d59a5562..77ba79ba 100644
--- a/packages/templatetags/package_extras.py
+++ b/packages/templatetags/package_extras.py
@@ -1,4 +1,9 @@
-import cgi, urllib
+from urllib import urlencode, quote as urlquote
+try:
+ from urlparse import parse_qs
+except ImportError:
+ from cgi import parse_qs
+
from django import template
from django.utils.html import escape
@@ -9,15 +14,15 @@ class BuildQueryStringNode(template.Node):
self.sortfield = sortfield
def render(self, context):
- qs = dict(cgi.parse_qsl(context['current_query'][1:]))
- if qs.has_key('sort') and qs['sort'] == self.sortfield:
+ qs = parse_qs(context['current_query'])
+ if qs.has_key('sort') and self.sortfield in qs['sort']:
if self.sortfield.startswith('-'):
- qs['sort'] = self.sortfield[1:]
+ qs['sort'] = [self.sortfield[1:]]
else:
- qs['sort'] = '-' + self.sortfield
+ qs['sort'] = ['-' + self.sortfield]
else:
- qs['sort'] = self.sortfield
- return '?' + urllib.urlencode(qs)
+ qs['sort'] = [self.sortfield]
+ return urlencode(qs, True)
@register.tag(name='buildsortqs')
def do_buildsortqs(parser, token):
@@ -43,4 +48,20 @@ def userpkgs(user):
)
return ''
+@register.simple_tag
+def bugs_list(package):
+ data = {
+ '@action': 'search'
+ 'title': package.pkgname,
+ }
+ return "https://bugs.parabolagnulinux.org/bugs/issue?%s" % urlencode(data)
+
+@register.simple_tag
+def bug_report(package):
+ data = {
+ '@template': 'item',
+ 'keyword': 'packages',
+ 'title': '[%s]' % package.pkgname,
+ }
+ return "https://bugs.parabolagnulinux.org/bugs/issue?" % 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/utils.py b/packages/utils.py
index 8d9f13ab..29a3087f 100644
--- a/packages/utils.py
+++ b/packages/utils.py
@@ -96,6 +96,8 @@ SELECT p.id, q.id
p.pkgver != q.pkgver
OR
p.pkgrel != q.pkgrel
+ OR
+ p.epoch != q.epoch
)
"""
cursor = connection.cursor()
diff --git a/packages/views.py b/packages/views.py
index 59779fe4..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
@@ -83,12 +84,39 @@ def update(request):
def details(request, name='', repo='', arch=''):
if all([name, repo, arch]):
- pkg = get_object_or_404(Package,
- pkgname=name, repo__name__iexact=repo, arch__name=arch)
- return direct_to_template(request, 'packages/details.html', {'pkg': pkg, })
+ try:
+ pkg = Package.objects.select_related(
+ 'arch', 'repo', 'packager').get(pkgname=name,
+ repo__name__iexact=repo, arch__name=arch)
+ return direct_to_template(request, 'packages/details.html',
+ {'pkg': pkg, })
+ except Package.DoesNotExist:
+ arch = get_object_or_404(Arch, name=arch)
+ arches = [ arch ]
+ arches.extend(Arch.objects.filter(agnostic=True))
+ repo = get_object_or_404(Repo, name__iexact=repo)
+ pkgs = Package.objects.filter(pkgbase=name,
+ repo__testing=repo.testing, arch__in=arches)
+ pkgs = pkgs.select_related('arch', 'repo').order_by('pkgname')
+ if len(pkgs) == 0:
+ raise Http404
+ context = {
+ 'list_title': 'Split Package Details',
+ 'name': name,
+ 'arch': arch,
+ 'packages': pkgs,
+ }
+ return direct_to_template(request, 'packages/packages_list.html',
+ context)
else:
- 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 = []
@@ -107,15 +135,16 @@ def group_details(request, arch, name):
arches = [ arch ]
arches.extend(Arch.objects.filter(agnostic=True))
pkgs = Package.objects.filter(groups__name=name, arch__in=arches)
- pkgs = pkgs.order_by('pkgname')
+ pkgs = pkgs.select_related('arch', 'repo').order_by('pkgname')
if len(pkgs) == 0:
raise Http404
context = {
- 'groupname': name,
+ 'list_title': 'Group Details',
+ 'name': name,
'arch': arch,
'packages': pkgs,
}
- return direct_to_template(request, 'packages/group_details.html', context)
+ return direct_to_template(request, 'packages/packages_list.html', context)
def getmaintainer(request, name, repo, arch):
"Returns the maintainers as plaintext."
@@ -126,6 +155,25 @@ def getmaintainer(request, name, repo, arch):
return HttpResponse(str('\n'.join(names)), mimetype='text/plain')
+def coerce_limit_value(value):
+ if not value:
+ return None
+ if value == 'all':
+ # negative value indicates show all results
+ return -1
+ value = int(value)
+ if value < 0:
+ raise ValueError
+ return value
+
+class LimitTypedChoiceField(forms.TypedChoiceField):
+ def valid_value(self, value):
+ try:
+ coerce_limit_value(value)
+ return True
+ except (ValueError, TypeError):
+ return False
+
class PackageSearchForm(forms.Form):
repo = forms.MultipleChoiceField(required=False)
arch = forms.MultipleChoiceField(required=False)
@@ -136,25 +184,12 @@ class PackageSearchForm(forms.Form):
flagged = forms.ChoiceField(
choices=[('', 'All')] + make_choice(['Flagged', 'Not Flagged']),
required=False)
- limit = forms.ChoiceField(
+ limit = LimitTypedChoiceField(
choices=make_choice([50, 100, 250]) + [('all', 'All')],
+ coerce=coerce_limit_value,
required=False,
initial=50)
- def clean_limit(self):
- limit = self.cleaned_data['limit']
- if limit == 'all':
- limit = None
- elif limit:
- try:
- limit = int(limit)
- except:
- raise forms.ValidationError("Should be an integer")
- else:
- limit = 50
- return limit
-
-
def __init__(self, *args, **kwargs):
super(PackageSearchForm, self).__init__(*args, **kwargs)
self.fields['repo'].choices = make_choice(
@@ -168,12 +203,10 @@ class PackageSearchForm(forms.Form):
[(m.username, m.get_full_name()) for m in maints]
def search(request, page=None):
- current_query = '?'
limit = 50
packages = Package.objects.select_related('arch', 'repo')
if request.GET:
- current_query += request.GET.urlencode()
form = PackageSearchForm(data=request.GET)
if form.is_valid():
if form.cleaned_data['repo']:
@@ -188,11 +221,12 @@ def search(request, page=None):
inner_q = PackageRelation.objects.all().values('pkgbase')
packages = packages.exclude(pkgbase__in=inner_q)
elif form.cleaned_data['maintainer']:
- inner_q = PackageRelation.objects.filter(user__username=form.cleaned_data['maintainer']).values('pkgbase')
+ inner_q = PackageRelation.objects.filter(
+ user__username=form.cleaned_data['maintainer']).values('pkgbase')
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)
@@ -204,16 +238,26 @@ def search(request, page=None):
lu = form.cleaned_data['last_update']
packages = packages.filter(last_update__gte=
datetime(lu.year, lu.month, lu.day, 0, 0))
- limit = form.cleaned_data['limit']
+
+ asked_limit = form.cleaned_data['limit']
+ if asked_limit and asked_limit < 0:
+ limit = None
+ elif asked_limit:
+ limit = asked_limit
+ else:
+ # Form had errors, don't return any results, just the busted form
+ packages = Package.objects.none()
else:
form = PackageSearchForm()
- page_dict = {'search_form': form,
- 'current_query': current_query
- }
if packages.count() == 1:
return redirect(packages[0])
+ current_query = request.GET.urlencode()
+ page_dict = {
+ 'search_form': form,
+ 'current_query': current_query
+ }
allowed_sort = ["arch", "repo", "pkgname", "last_update", "flag_date"]
allowed_sort += ["-" + s for s in allowed_sort]
sort = request.GET.get('sort', None)
@@ -323,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:
@@ -350,28 +397,44 @@ 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),
- 'Arch Website Notification <nobody@archlinux.org>',
+ 'Parabola Packages <packages@list.parabolagnulinux.org>',
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)
@@ -382,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):
@@ -404,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 81f589f7..fd29a845 100644
--- a/public/utils.py
+++ b/public/utils.py
@@ -3,30 +3,78 @@ 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():
+def get_recent_updates(number=15):
# This is a bit of magic. We are going to show 15 on the front page, but we
# want to try and eliminate cross-architecture wasted space. Pull enough
# 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 * 6
for arch in Arch.objects.all():
- # grab a few extra so we can hopefully catch everything we need
pkgs += list(Package.objects.select_related(
- 'arch', 'repo').filter(arch=arch).order_by('-last_update')[:50])
+ 'arch', 'repo').filter(arch=arch).order_by('-last_update')[:fetch])
pkgs.sort(key=attrgetter('last_update'))
+
updates = []
- ctr = 0
- while ctr < 15 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..d2d4eb80
--- /dev/null
+++ b/releng/fixtures/clockchoices.json
@@ -0,0 +1,72 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "default region/timezone, keep clock"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "default region/timezone, change clock manually (UTC)"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "default region/timezone, change clock with NTP (UTC)"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "default region/timezone, change clock manually (localtime)"
+ }
+ },
+ {
+ "pk": 5,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "default region/timezone, change clock with NTP (localtime)"
+ }
+ },
+ {
+ "pk": 6,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "update region/timezone, keep clock"
+ }
+ },
+ {
+ "pk": 7,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "update region/timezone, change clock manually (UTC)"
+ }
+ },
+ {
+ "pk": 8,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "update region/timezone, change clock with NTP (UTC)"
+ }
+ },
+ {
+ "pk": 9,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "update region/timezone, change clock manually (localtime)"
+ }
+ },
+ {
+ "pk": 10,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "update region/timezone, change clock with NTP (localtime)"
+ }
+ }
+]
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..3180a9ed
--- /dev/null
+++ b/releng/views.py
@@ -0,0 +1,137 @@
+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).order_by('-id'))
+ 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 7d3c500f..e9285560 100644
--- a/settings.py
+++ b/settings.py
@@ -4,6 +4,7 @@ import os
## Set the debug values
DEBUG = False
TEMPLATE_DEBUG = DEBUG
+DEBUG_TOOLBAR = False
## Notification admins
ADMINS = ()
@@ -44,13 +45,6 @@ LOGIN_REDIRECT_URL = '/'
# Set django's User stuff to use our profile model
AUTH_PROFILE_MODULE = 'main.UserProfile'
-# List of callables that know how to import templates from various sources.
-TEMPLATE_LOADERS = (
- 'django.template.loaders.filesystem.load_template_source',
- 'django.template.loaders.eggs.load_template_source',
- 'django.template.loaders.app_directories.load_template_source',
-)
-
# We add a processor to determine if the request is secure or not
TEMPLATE_CONTEXT_PROCESSORS = (
'django.contrib.auth.context_processors.auth',
@@ -61,6 +55,18 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'main.context_processors.secure',
)
+TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates".
+ # Always use forward slashes, even on Windows.
+ '%s/templates' % DEPLOY_PATH,
+)
+
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.eggs.Loader',
+ 'django.template.loaders.app_directories.Loader',
+)
+
# This bug is a real bummer:
# http://code.djangoproject.com/ticket/14105
MIDDLEWARE_CLASSES = (
@@ -77,18 +83,6 @@ MIDDLEWARE_CLASSES = (
ROOT_URLCONF = 'urls'
-TEMPLATE_DIRS = (
- # Put strings here, like "/home/html/django_templates".
- # Always use forward slashes, even on Windows.
- '%s/templates' % DEPLOY_PATH,
-)
-
-TEMPLATE_LOADERS = (
- 'django.template.loaders.filesystem.Loader',
- 'django.template.loaders.eggs.Loader',
- 'django.template.loaders.app_directories.Loader',
-)
-
# Configure where sessions and messages should reside
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
@@ -110,6 +104,7 @@ INSTALLED_APPS = (
'devel',
'public',
'south', # database migration support
+ 'releng',
)
## Import local settings
@@ -121,4 +116,15 @@ if not TEMPLATE_DEBUG:
('django.template.loaders.cached.Loader', TEMPLATE_LOADERS),
)
+# Enable the debug toolbar if requested
+if DEBUG_TOOLBAR:
+ MIDDLEWARE_CLASSES = \
+ [ 'debug_toolbar.middleware.DebugToolbarMiddleware' ] + \
+ list(MIDDLEWARE_CLASSES)
+
+ INSTALLED_APPS = list(INSTALLED_APPS) + [ 'debug_toolbar' ]
+
+# URL to fetch a current list of available ISOs
+ISO_LIST_URL = 'http://repo.parabolagnulinux.org/isos/'
+
# vim: set ts=4 sw=4 et:
diff --git a/templates/admin/index.html b/templates/admin/index.html
index 6f7f98ee..93cf258d 100644
--- a/templates/admin/index.html
+++ b/templates/admin/index.html
@@ -14,10 +14,18 @@
<div class="module">
<table>
<caption>Custom Admin Pages</caption>
+ {% if perms.auth.add_user %}
<tr>
<th scope="row"><a href="/devel/newuser/">Create New User</a></th>
<td></td><td></td>
</tr>
+ {% endif %}
+ {% if user.is_superuser %}
+ <tr>
+ <th scope="row"><a href="/devel/admin_log/">Admin Actions Log</a></th>
+ <td></td><td></td>
+ </tr>
+ {% endif %}
</table>
</div>
diff --git a/templates/devel/admin_log.html b/templates/devel/admin_log.html
new file mode 100644
index 00000000..0f22ba2b
--- /dev/null
+++ b/templates/devel/admin_log.html
@@ -0,0 +1,62 @@
+{% extends "admin/base_site.html" %}
+{% load i18n %}
+
+{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% load adminmedia %}{% admin_media_prefix %}css/dashboard.css" />{% endblock %}
+
+{% block breadcrumbs %}<div class="breadcrumbs"><a href="/admin/">{% trans 'Home' %}</a>{% if title %} &rsaquo; {{ title }}{% endif %}</div>{% endblock %}
+
+{% block content %}
+<div id="content-main">
+ <div class="module">
+{% load log %}
+{% if log_user %}
+{% get_admin_log 100 as admin_log for_user log_user %}
+{% else %}
+{% get_admin_log 100 as admin_log %}
+{% endif %}
+{% if not admin_log %}
+<p>{% trans 'None available' %}</p>
+{% else %}
+<table id="change-history">
+ <thead>
+ <tr>
+ <th scope="col">{% trans 'Date/time' %}</th>
+ <th scope="col">{% trans 'User' %}</th>
+ <th>Type</th>
+ <th>Object</th>
+ <th scope="col">{% trans 'Action' %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for entry in admin_log %}
+ <tr>
+ <th scope="row">{{ entry.action_time|date:"DATETIME_FORMAT" }}</th>
+ {% if log_user %}
+ <td>{{ entry.user.username }}{% if entry.user.get_full_name %} ({{ entry.user.get_full_name }}){% endif %}</td>
+ {% else %}
+ <td><a href="{{ entry.user.username }}/">{{ entry.user.username }}</a>{% if entry.user.get_full_name %} ({{ entry.user.get_full_name }}){% endif %}</td>
+ {% endif %}
+ <td>
+ {% if entry.content_type %}
+ <span>{% filter capfirst %}{% trans entry.content_type.name %}{% endfilter %}</span>
+ {% else %}
+ <span>{% trans 'Unknown content' %}</span>
+ {% endif %}
+ </td>
+ <td>
+ <span class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}"></span>
+ {% if entry.is_deletion %}
+ {{ entry.object_repr }}
+ {% else %}
+ <a href="/admin/{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
+ {% endif %}
+ </td>
+ <td>{{ entry.change_message }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% endif %}
+ </div>
+</div>
+{% endblock %}
diff --git a/templates/devel/clock.html b/templates/devel/clock.html
index ebc3b8bf..cbf4b834 100644
--- a/templates/devel/clock.html
+++ b/templates/devel/clock.html
@@ -14,11 +14,12 @@
UTC Time: {{ utc_now|date:"Y-m-d H:i T" }}
</p>
- <table id="clocks-table" class="results dash-stats">
+ <table id="clocks-table" class="results">
<thead>
<tr>
<th>Hacker</th>
<th>Username</th>
+ <th>Alias</th>
<th>Location</th>
<th>Time Zone</th>
<th>Current Time</th>
@@ -29,6 +30,7 @@
<tr class="{% cycle 'odd' 'even' %}">
<td><a href="mailto:{{ dev.email }}">{{ dev.get_full_name }}</a></td>
<td>{{ dev.username }}</td>
+ <td>{{ dev.userprofile.alias }}</td>
<td>{{ dev.userprofile.location }}</td>
<td>{{ dev.userprofile.time_zone }}</td>
<td>{{ dev.current_time|date:"Y-m-d H:i T" }}</td>
diff --git a/templates/devel/index.html b/templates/devel/index.html
index b681a96e..7b728a91 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">
@@ -8,7 +10,7 @@
<h3>My Flagged Packages</h3>
- <table id="dash-myflagged" class="results dash-stats">
+ <table id="dash-myflagged" class="results">
<thead>
<tr>
<th>Name</th>
@@ -27,8 +29,8 @@
<td>{{ pkg.repo.name }}</td>
<td>{{ pkg.pkgver }}</td>
<td>{{ pkg.arch.name }}</td>
- <td>{{ pkg.flag_date }}</td>
- <td>{{ pkg.last_update }}</td>
+ <td>{{ pkg.flag_date|date }}</td>
+ <td>{{ pkg.last_update|date }}</td>
</tr>
{% empty %}
<tr class="empty"><td colspan="4"><em>No flagged packages to display</em></td></tr>
@@ -38,7 +40,7 @@
<h3>My Incomplete Todo List Packages</h3>
- <table id="dash-mytodolist" class="results dash-stats">
+ <table id="dash-mytodolist" class="results">
<thead>
<tr>
<th>Todo List</th>
@@ -67,35 +69,60 @@
<h3>Package Todo Lists</h3>
- <table id="dash-todo" class="results dash-stats">
+ <table id="dash-todo" class="results">
<thead>
<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 }}</td>
- <td class="wrap">{{ todo.description|safe }}</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
+ (<a href="reports/big/{{ user.username }}/">yours only</a>)</li>
+ <li><a href="reports/old/">Old</a>:
+ Packages last built more than two years ago
+ (<a href="reports/old/{{ user.username }}/">yours only</a>)</li>
+ <li><a href="reports/uncompressed-man/">Uncompressed Manpages</a>:
+ Self-explanatory
+ (<a href="reports/uncompressed-man/{{ user.username }}/">yours only</a>)</li>
+ <li><a href="reports/uncompressed-info/">Uncompressed Info Pages</a>:
+ Self-explanatory
+ (<a href="reports/uncompressed-info/{{ user.username }}/">yours only</a>)</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 -->
-<div id="dash-by-arch" class="dash-stats box">
+{% cache 60 dev-dash-by-arch %}
+<div id="dash-by-arch" class="box">
- <h3 class="dash-stats" style="cursor: pointer"
- title="Click to toggle stats by architecture">
- Stats by Architecture <span class="dash-click">(click to toggle)</span></h3>
+ <h2>Stats by Architecture</h2>
<table id="stats-by-arch" class="results dash-stats">
<thead>
@@ -119,14 +146,13 @@
{% endfor %}
</tbody>
</table>
+</div>{# #dash-by-arch #}
+{% endcache %}
-</div><!-- #dash-by-arch -->
-
-<div id="dash-by-repo" class="dash-stats box">
+{% cache 60 dev-dash-by-repo %}
+<div id="dash-by-repo" class="box">
- <h3 class="dashboard dash-stats" style="cursor: pointer"
- title="Click to toggle stats by repository">
- Stats by Repository <span class="dash-click">(click to toggle)</span></h3>
+ <h2>Stats by Repository</h2>
<table id="stats-by-repo" class="results dash-stats">
<thead>
@@ -150,14 +176,13 @@
{% 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">
-<div id="dash-by-maintainer" class="dash-stats box">
-
- <h3 class="dashboard dash-stats" style="cursor: pointer"
- title="Click to toggle stats by maintainer">
- Stats by Maintainer <span class="dash-click">(click to toggle)</span></h3>
+ <h2>Stats by Maintainer</h2>
<table id="stats-by-maintainer" class="results dash-stats">
<thead>
@@ -166,6 +191,15 @@
<th># Packages</th>
<th># Flagged</th>
</tr>
+ <tr class="even">
+ <td><em>Orphan</em></td>
+ <td><a href="/packages/?maintainer=orphan"
+ title="View all orphan packages">
+ <strong>{{ orphan.package_count }}</strong> packages</a></td>
+ <td><a href="/packages/?maintainer=orphan&amp;flagged=Flagged"
+ title="View all flagged orphan packages">
+ <strong>{{ orphan.flagged_count }}</strong> packages</a></td>
+ </tr>
</thead>
<tbody>
{% for maint in maintainers %}
@@ -181,8 +215,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>
@@ -194,12 +229,9 @@ $(document).ready(function() {
{widgets: ['zebra'], sortList: [[0,0], [1,0]]});
$("#dash-todo:not(:has(tbody tr.empty))").tablesorter(
{widgets: ['zebra'], sortList: [[1,1]]});
- $("#stats-by-arch").add("#stats-by-repo").add("#stats-by-maintainer").tablesorter(
+ $(".dash-stats").tablesorter(
{widgets: ['zebra'], sortList: [[0,0]],
headers: { 1: { sorter: 'pkgcount' }, 2: { sorter: 'pkgcount' } } });
- $("h3.dash-stats").click(
- function(e) { $(this).next().toggle(); }
- );
});
</script>
{% endblock %}
diff --git a/templates/devel/packages.html b/templates/devel/packages.html
new file mode 100644
index 00000000..b8deb0bc
--- /dev/null
+++ b/templates/devel/packages.html
@@ -0,0 +1,62 @@
+{% extends "base.html" %}
+{% load attributes %}
+
+{% block title %}Parabola - {{ title }}{% endblock %}
+
+{% block content %}
+<div class="box">
+ <h2>{{ title }}{% if maintainer %},
+ maintained by {{ maintainer.get_full_name }}{% endif%}</h2>
+ <p>{{ packages|length }} package{{ packages|pluralize }} found.
+ {% if maintainer %}This report only includes packages maintained by
+ {{ maintainer.get_full_name }} ({{ maintainer.username }}).{% endif %}
+ </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 910c6207..f92ac684 100644
--- a/templates/feeds/packages_title.html
+++ b/templates/feeds/packages_title.html
@@ -1 +1 @@
-{{ obj.pkgname }} {{ obj.pkgver }}-{{ obj.pkgrel }} {{ 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/news/add.html b/templates/news/add.html
index 0534826c..fd75253c 100644
--- a/templates/news/add.html
+++ b/templates/news/add.html
@@ -23,14 +23,14 @@
<p>
<label></label>
<input title="Save changes" type="submit" value="Save" />
- <input id="previewbtn" title="Preview" type="button" value="Preview" />
+ <input id="news-preview-button" title="Preview" type="button" value="Preview" />
</p>
</form>
</div>
-<div class="news-article box" style="display:none;">
- <h2>News Preview: <span id="previewtitle"></span></h2>
- <div id="previewdata" class="article-content"></div>
+<div id="news-preview" class="news-article box" style="display:none;">
+ <h2>News Preview: <span id="news-preview-title"></span></h2>
+ <div id="news-preview-data" class="article-content"></div>
</div>
{% load cdn %}{% jquery %}
<script type="text/javascript" src="/media/archweb.js"></script>
diff --git a/templates/packages/details.html b/templates/packages/details.html
index 4fae6c68..f1b16e4c 100644
--- a/templates/packages/details.html
+++ b/templates/packages/details.html
@@ -1,29 +1,28 @@
{% extends "base.html" %}
{% load cache %}
-{% block title %}Parabola - {{ pkg.pkgname }} {{ pkg.pkgver }}-{{ pkg.pkgrel }} - Package Details{% endblock %}
+{% block title %}Parabola - {{ pkg.pkgname }} {{ pkg.full_version }} - Package Details{% endblock %}
{% block navbarclass %}anb-packages{% endblock %}
{% load package_extras %}
{% block content %}
<div id="pkgdetails" class="box">
- <h2>Package Details: {{ pkg.pkgname }} {{ pkg.pkgver }}-{{ pkg.pkgrel }}</h2>
+ <h2>Package Details: {{ pkg.pkgname }} {{ pkg.full_version }}</h2>
<div id="detailslinks" class="listing">
<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="{% 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 %}
<li><span class="flagged">Version
<a href="{{ tp.get_absolute_url }}"
- title="Testing package details for {{ tp.pkgname }}">{{ tp.pkgver }}-{{ tp.pkgrel }}</a>
+ title="Testing package details for {{ tp.pkgname }}">{{ tp.full_version }}</a>
in testing</span></li>
{% endif %}{% endwith %}
{% if perms.main.change_package %}
@@ -58,7 +57,7 @@
<ul>
{% for o in others %}
<li><a href="{{ o.get_absolute_url }}"
- title="Package details for {{ o.pkgname }}">{{ o.pkgname }} {{ o.pkgver }}-{{ o.pkgrel }} [{{ o.repo.name|lower }}] ({{ o.arch.name }})</a></li>
+ title="Package details for {{ o.pkgname }}">{{ o.pkgname }} {{ o.full_version }} [{{ o.repo.name|lower }}] ({{ o.arch.name }})</a></li>
{% endfor %}
</ul>
</div>
@@ -96,13 +95,14 @@
<td><a href="{{ pkg.base_package.get_absolute_url }}"
title="Package details for {{ pkg.base_package.pkgname }}">{{ pkg.pkgbase }}</a></td>
{% else %}
- <td>{{ pkg.pkgbase }}</td>
+ <td><a href="../{{ pkg.pkgbase }}/"
+ title="Split package details for {{ pkg.pkgbase }}">{{ pkg.pkgbase }}</a></td>
{% endif %}
</tr>
{% 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/differences.html b/templates/packages/differences.html
index ba2d9bf5..69c39756 100644
--- a/templates/packages/differences.html
+++ b/templates/packages/differences.html
@@ -45,12 +45,12 @@
{% if diff.pkg_a %}
<td><a href="{{ diff.pkg_a.get_absolute_url }}"
title="View package details for {{ diff.pkg_a.pkgname }}">
- <span{% if diff.pkg_a.flag_date %} class="flagged"{% endif %}>{{ diff.pkg_a.pkgver }}-{{ diff.pkg_a.pkgrel }}</span></a></td>
+ <span{% if diff.pkg_a.flag_date %} class="flagged"{% endif %}>{{ diff.pkg_a.full_version }}</span></a></td>
{% else %}<td>-</td>{% endif %}
{% if diff.pkg_b %}
<td><a href="{{ diff.pkg_b.get_absolute_url }}"
title="View package details for {{ diff.pkg_b.pkgname }}">
- <span{% if diff.pkg_b.flag_date %} class="flagged"{% endif %}>{{ diff.pkg_b.pkgver }}-{{ diff.pkg_b.pkgrel }}</span></a></td>
+ <span{% if diff.pkg_b.flag_date %} class="flagged"{% endif %}>{{ diff.pkg_b.full_version }}</span></a></td>
{% else %}<td>-</td>{% endif %}
</tr>
{% endfor %}
diff --git a/templates/packages/files.html b/templates/packages/files.html
index cd6b4c46..149154a6 100644
--- a/templates/packages/files.html
+++ b/templates/packages/files.html
@@ -1,11 +1,11 @@
{% extends "base.html" %}
-{% block title %}Parabola - {{ pkg.pkgname }} {{ pkg.pkgver }}-{{ pkg.pkgrel }} - Package File List{% endblock %}
+{% block title %}Parabola - {{ pkg.pkgname }} {{ pkg.full_version }} - Package File List{% endblock %}
{% block navbarclass %}anb-packages{% endblock %}
{% block content %}
<div id="pkgdetails" class="box">
- <h2>Package File List: {{ pkg.pkgname }} {{ pkg.pkgver }}-{{ pkg.pkgrel }}</h2>
+ <h2>Package File List: {{ pkg.pkgname }} {{ pkg.full_version }}</h2>
<div id="metadata">
<p><a href="{{ pkg.get_absolute_url }}">Back to Package</a></p>
{% include "packages/files-list.html" %}
diff --git a/templates/packages/flag.html b/templates/packages/flag.html
index d947b573..74f6982c 100644
--- a/templates/packages/flag.html
+++ b/templates/packages/flag.html
@@ -1,42 +1,40 @@
{% 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
- an email to the <a href="http://mailman.archlinux.org/mailman/listinfo/arch-general"
- title="Visit the arch-general mailing list">arch-general mailing list</a>
+ an email to the <a href="http://list.parabolagnulinux.org/listinfo.cgi/dev-parabolagnulinux.org"
+ title="Visit the dev mailing list">Parabola Development mailing list</a>
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.archlinux.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..baa466dd
--- /dev/null
+++ b/templates/packages/flag_confirmed.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+{% block title %}Parabola - 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/flaghelp.html b/templates/packages/flaghelp.html
index d96ccd43..4a9d1cdf 100644
--- a/templates/packages/flaghelp.html
+++ b/templates/packages/flaghelp.html
@@ -25,12 +25,13 @@
<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
- an email to the <a target="_blank" href="http://mailman.archlinux.org/mailman/listinfo/arch-general"
- title="Visit the arch-general mailing list">arch-general mailing list</a>
+ an email to the <a target="_blank"
+ href="http://list.parabolagnulinux.org/listinfo.cgi/dev-parabolagnulinux.org"
+ title="Visit the parabola dev mailing list">parabola mailing list</a>
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 target="_blank" href="https://bugs.archlinux.org"
+ package is broken! Use the <a target="_blank" href="https://bugs.parabolagnulinux.org"
title="Parabola Bugtracker">bugtracker</a> instead.</p>
</body>
diff --git a/templates/packages/opensearch.xml b/templates/packages/opensearch.xml
index ea040788..e54507ee 100644
--- a/templates/packages/opensearch.xml
+++ b/templates/packages/opensearch.xml
@@ -2,7 +2,7 @@
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>Parabola Packages</ShortName>
<Description>Search the Parabola package repositories.</Description>
- <Tags>linux archlinux package software</Tags>
+ <Tags>gnu linuxlibre parabola package software</Tags>
<Image height="16" width="16" type="image/x-icon">{{domain}}/media/favicon.ico</Image>
<Language>en-us</Language>
<InputEncoding>UTF-8</InputEncoding>
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/group_details.html b/templates/packages/packages_list.html
index ee4f61d3..2508d8fd 100644
--- a/templates/packages/group_details.html
+++ b/templates/packages/packages_list.html
@@ -1,10 +1,10 @@
{% extends "base.html" %}
-{% block title %}Parabola - {{ groupname }} - {{ arch.name }} - Group Details{% endblock %}
+{% block title %}Parabola - {{ name }} ({{ arch.name }}) - {{ list_title }}{% endblock %}
{% block navbarclass %}anb-packages{% endblock %}
{% block content %}
<div class="box">
- <h2>Details for group {{ groupname }} - {{ arch.name }}</h2>
+ <h2>{{ list_title }} - {{ name }} ({{ arch.name }})</h2>
<table class="results">
<thead>
<tr>
@@ -14,6 +14,7 @@
<th>Version</th>
<th>Description</th>
<th>Last Updated</th>
+ <th>Flag Date</th>
</tr>
</thead>
<tbody>
@@ -24,12 +25,13 @@
<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.pkgver }}-{{ pkg.pkgrel }}</span></td>
+ <td><span class="flagged">{{ pkg.full_version }}</span></td>
{% else %}
- <td>{{ pkg.pkgver }}-{{ pkg.pkgrel }}</td>
+ <td>{{ pkg.full_version }}</td>
{% endif %}
<td class="wrap">{{ pkg.pkgdesc }}</td>
<td>{{ pkg.last_update|date }}</td>
+ <td>{{ pkg.flag_date|date }}</td>
</tr>
{% endfor %}
</tbody>
diff --git a/templates/packages/search.html b/templates/packages/search.html
index 74f7ede3..ae9e55f2 100644
--- a/templates/packages/search.html
+++ b/templates/packages/search.html
@@ -14,28 +14,36 @@
<h3>Package Search</h3>
- <form id="pkg-search" method="get" action="/packages/">
- <p><input type="hidden" name="sort" value='{{sort}}' /></p>
- <fieldset>
- <legend>Enter search criteria</legend>
- <div><label for="id_arch" title="Limit results a specific CPU architecture">
- Arch</label>{{ search_form.arch }}</div>
- <div><label for="id_repo" title="Limit results to a specific respository">
- Repository</label>{{ search_form.repo }}</div>
- <div><label for="id_q" title="Enter keywords as desired">
- Keywords</label>{{ search_form.q }}</div>
- <div><label for="id_maintainer" title="Limit results to a specific maintainer">
- Maintainer</label>{{ search_form.maintainer}}</div>
- <div><label for="id_last_update" title="Limit results to a date after the date entered">
- Last Updated After</label>{{ search_form.last_update }}</div>
- <div><label for="id_flagged" title="Limit results based on out-of-date status">
- Flagged</label>{{ search_form.flagged }}</div>
- <div><label for="id_limit" title="Select the number of results to display per page">
- Per Page</label>{{ search_form.limit }}</div>
- <div ><label>&nbsp;</label><input title="Search for packages using this criteria"
- type="submit" value="Search" /></div>
- </fieldset>
- </form>
+ <form id="pkg-search" method="get" action="/packages/">
+ <p><input type="hidden" name="sort" value='{{sort}}' /></p>
+ {{ search_form.non_field_errors }}
+ <fieldset>
+ <legend>Enter search criteria</legend>
+ <div>{{ search_form.arch.errors }}
+ <label for="id_arch" title="Limit results a specific CPU architecture">
+ Arch</label>{{ search_form.arch }}</div>
+ <div>{{ search_form.repo.errors }}
+ <label for="id_repo" title="Limit results to a specific respository">
+ Repository</label>{{ search_form.repo }}</div>
+ <div>{{ search_form.q.errors }}
+ <label for="id_q" title="Enter keywords as desired">
+ Keywords</label>{{ search_form.q }}</div>
+ <div>{{ search_form.maintainer.errors }}
+ <label for="id_maintainer" title="Limit results to a specific maintainer">
+ Maintainer</label>{{ search_form.maintainer}}</div>
+ <div>{{ search_form.last_update.errors }}
+ <label for="id_last_update" title="Limit results to a date after the date entered">
+ Last Updated After</label>{{ search_form.last_update }}</div>
+ <div>{{ search_form.flagged.errors }}
+ <label for="id_flagged" title="Limit results based on out-of-date status">
+ Flagged</label>{{ search_form.flagged }}</div>
+ <div>{{ search_form.limit.errors }}
+ <label for="id_limit" title="Select the number of results to display per page">
+ Per Page</label>{{ search_form.limit }}</div>
+ <div ><label>&nbsp;</label><input title="Search for packages using this criteria"
+ type="submit" value="Search" /></div>
+ </fieldset>
+ </form>
</div><!-- #pkglist-search -->
@@ -50,13 +58,13 @@
<p class="pkglist-nav">
{% if page_obj.has_previous %}
- <a class="prev" href="/packages/{{page_obj.previous_page_number}}/{{current_query}}"
+ <a class="prev" href="/packages/{{page_obj.previous_page_number}}/?{{current_query}}"
title="Go to previous page">&lt; Prev</a>
{% else %}
<span class="prev">&lt; Prev</span>
{% endif %}
{% if page_obj.has_next %}
- <a class="next" href="/packages/{{page_obj.next_page_number}}/{{current_query}}"
+ <a class="next" href="/packages/{{page_obj.next_page_number}}/?{{current_query}}"
title="Go to next page">Next &gt;</a>
{% else %}
<span class="next">Next &gt;</span>
@@ -74,17 +82,17 @@
{% if perms.main.change_package %}
<th>&nbsp;</th>
{% endif %}
- <th><a href="/packages/{% buildsortqs "arch" %}"
+ <th><a href="/packages/?{% buildsortqs "arch" %}"
title="Sort packages by architecture">Arch</a></th>
- <th><a href="/packages/{% buildsortqs "repo" %}"
+ <th><a href="/packages/?{% buildsortqs "repo" %}"
title="Sort packages by repository">Repo</a></th>
- <th><a href="/packages/{% buildsortqs "pkgname" %}"
+ <th><a href="/packages/?{% buildsortqs "pkgname" %}"
title="Sort packages by package name">Name</a></th>
<th>Version</th>
<th>Description</th>
- <th><a href="/packages/{% buildsortqs "-last_update" %}"
+ <th><a href="/packages/?{% buildsortqs "-last_update" %}"
title="Sort packages by last update">Last Updated</a></th>
- <th><a href="/packages/{% buildsortqs "-flag_date" %}"
+ <th><a href="/packages/?{% buildsortqs "-flag_date" %}"
title="Sort packages by when marked-out of-date">Flag Date</a></th>
</tr>
</thead>
@@ -99,9 +107,9 @@
<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.pkgver }}-{{ pkg.pkgrel }}</span></td>
+ <td><span class="flagged">{{ pkg.full_version }}</span></td>
{% else %}
- <td>{{ pkg.pkgver }}-{{ pkg.pkgrel }}</td>
+ <td>{{ pkg.full_version }}</td>
{% endif %}
<td class="wrap">{{ pkg.pkgdesc }}</td>
<td>{{ pkg.last_update|date }}</td>
@@ -118,13 +126,13 @@
<p class="pkglist-nav">
{% if page_obj.has_previous %}
- <a class="prev" href="/packages/{{page_obj.previous_page_number}}/{{current_query}}"
+ <a class="prev" href="/packages/{{page_obj.previous_page_number}}/?{{current_query}}"
title="Go to previous page">&lt; Prev</a>
{% else %}
<span class="prev">&lt; Prev</span>
{% endif %}
{% if page_obj.has_next %}
- <a class="next" href="/packages/{{page_obj.next_page_number}}/{{current_query}}"
+ <a class="next" href="/packages/{{page_obj.next_page_number}}/?{{current_query}}"
title="Go to next page">Next &gt;</a>
{% else %}
<span class="next">Next &gt;</span>
@@ -147,18 +155,17 @@
{% else %}
<div class="box">
<p>We couldn't find any packages matching your query. Try searching again
- using different criteria, or try
- {% if search_form.q.data %}
- <a href="https://aur.archlinux.org/packages.php?K={{ search_form.q.data }}">searching the AUR</a>
- {% else %}searching the AUR{% endif %}
- to see if the package can be found there.</p>
+ using different criteria.</p>
</div>
{% endif %}
<div id="pkglist-about" class="box">
- <p>You are browsing the Parabola package database. From here you can find
- detailed information about packages located in the official supported repositories.</p>
-</div>
+ <p>You are browsing the Parabola package database. From here you can
+ find detailed information about packages located in the official
+ supported repositories. If you need the sourceball from where a
+ package is built, you can look at our <a
+ href='http://repo.parabolagnulinux.org/sources/packages'
+ title='Sourceballed packages'>sources repo</a>.</p> </div>
<script type="text/javascript" src="/jsi18n/"></script>
{% load adminmedia %}<script type="text/javascript" src="{% admin_media_prefix %}js/core.js"></script>
diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html
index a9cce4a8..157843ac 100644
--- a/templates/packages/signoffs.html
+++ b/templates/packages/signoffs.html
@@ -26,7 +26,7 @@
<td>{{ pkg.arch.name }}</td>
<td><a href="{{ pkg.get_absolute_url }}"
title="View package details for {{ pkg.pkgname }}">{{ pkg.pkgname }}</a></td>
- <td>{{ pkg.pkgver }}-{{ pkg.pkgrel }}</td>
+ <td>{{ pkg.full_version }}</td>
<td>{{ pkg.last_update }}</td>
<td>{{ target }}</td>
<td class="signoff-{{pkg.approved_for_signoff|yesno}}">
diff --git a/templates/packages/stale_relations.html b/templates/packages/stale_relations.html
index 8e2f8930..6cff8d3d 100644
--- a/templates/packages/stale_relations.html
+++ b/templates/packages/stale_relations.html
@@ -1,5 +1,5 @@
{% extends "base.html" %}
-{% block title %}Arch Linux - Stale Package Relations{% endblock %}
+{% block title %}Parabola - Stale Package Relations{% endblock %}
{% block navbarclass %}anb-packages{% endblock %}
{% block content %}
@@ -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/feeds.html b/templates/public/feeds.html
index 5aa5df20..79e8a1aa 100644
--- a/templates/public/feeds.html
+++ b/templates/public/feeds.html
@@ -6,7 +6,7 @@
<h2>RSS Feeds</h2>
- <p>Several RSS feeds are available for consumption from the Arch website.
+ <p>Several RSS feeds are available for consumption from the Parabola website.
The majority of these are package-related and allow feeds to be customized
for the updates you care about.</p>
@@ -58,12 +58,5 @@
</tbody>
</table>
- <p>A <a href="https://aur.archlinux.org/rss.php" class="rss" title="AUR newest packages feed">newest packages feed</a>
- is also available from the <a href="https://aur.archlinux.org/" title="AUR
- Homepage">Arch User Repository (AUR)</a>, but be specially aware that sometimes,
- unfree software gets uploaded there, <em>at least until we found a solution
- to it.</em></p>
-
-
</div>
{% endblock %}
diff --git a/templates/public/index.html b/templates/public/index.html
index ff11994f..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.pkgver }}-{{ fpkg.pkgrel }}</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/public/svn.html b/templates/public/svn.html
index 10a58ba6..80367eaf 100644
--- a/templates/public/svn.html
+++ b/templates/public/svn.html
@@ -3,38 +3,11 @@
{% block content %}
<div class="box">
<h2 class="title">SVN Repositories</h2>
- <p>
- The PKGBUILD files can be fetched via the ABS utility. To learn more
- about ABS, see <a href="https://wiki.archlinux.org/index.php/ABS">the ABS wiki page</a>.
- </p>
- <p>The SVN repositories have been cloned into git repositories and can be
- viewed via the cgit interface.
- <a href="http://projects.archlinux.org/svntogit/packages.git/">All
- packages</a> are available here except for
- <a href="http://projects.archlinux.org/svntogit/community.git/">community
- and multilib</a> which are available in a different repository.</p>
- <p>
- You can also get individual PKGBUILDs directly from SVN. This can be
- especially useful if you need to compile an older version of a package.
- <strong>DO NOT CHECK OUT THE ENTIRE SVN REPO</strong>. Your address may be
- blocked. Use the following commands to check out a specific package:
+ <p>Parabola doesn't use any SVN repositories. But you can find our <a
+ href="https://projects.parabolagnulinux.org" title="Projects
+ page">Projects</a> on git!
</p>
- <pre>svn checkout --depth=empty svn://svn.archlinux.org/packages
-cd packages
-svn update &lt;your-package-name&gt;</pre>
-
- For the community and multilib repositories, use the following commands
- instead:
- <pre>svn checkout --depth=empty svn://svn.archlinux.org/community
-cd community
-svn update &lt;your-package-name&gt;</pre>
-
- <p>
- Visit <a href="https://wiki.archlinux.org/index.php?title=Getting_PKGBUILDS_From_SVN">the wiki</a>
- for more tips on checking out and updating svn PKGBUILDs.
- </p>
-
</div>
<br /><br />
{% endblock %}
diff --git a/templates/releng/add.html b/templates/releng/add.html
new file mode 100644
index 00000000..428812c9
--- /dev/null
+++ b/templates/releng/add.html
@@ -0,0 +1,27 @@
+{% extends "base.html" %}
+
+{% block title %}Parabola - Test Result Entry{% endblock %}
+
+{% block content %}
+<div class="box">
+ <h2>Parabola Releng Testbuild Feedback Entry</h2>
+
+ <p>This page allows you to submit feedback after testing an Parabola
+ 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..6f7d29f0
--- /dev/null
+++ b/templates/releng/results.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+
+{% block title %}Parabola - 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>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..984a056d
--- /dev/null
+++ b/templates/releng/thanks.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+
+{% block title %}Parabola - 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/templates/todolists/list.html b/templates/todolists/list.html
index a58fedc8..38d491d9 100644
--- a/templates/todolists/list.html
+++ b/templates/todolists/list.html
@@ -30,9 +30,9 @@
<tr class="{% cycle 'odd' 'even' %}">
<td><a href="{{ list.get_absolute_url }}"
title="View todo list: {{ list.name }}">{{ list.name }}</a></td>
- <td>{{ list.date_added }}</td>
+ <td>{{ list.date_added|date }}</td>
<td>{{ list.creator.get_full_name }}</td>
- <td class="wrap">{{ list.description|safe }}</td>
+ <td class="wrap">{{ list.description|urlize }}</td>
<td>{{ list.pkg_count }}</td>
<td>{{ list.incomplete_count }}</td>
<td>{% ifequal list.incomplete_count 0 %}<span class="complete">Complete</span>
diff --git a/templates/todolists/public_list.html b/templates/todolists/public_list.html
index 5b957692..ceb001de 100644
--- a/templates/todolists/public_list.html
+++ b/templates/todolists/public_list.html
@@ -22,13 +22,14 @@
</div>
</div>
{% if todo_lists %}
-<div id="public_todo_lists">
+<div id="public-todo-lists">
{% for list in todo_lists %}
<div class="box">
- <a name="{{ list.id }}"></a>
- <h4>{{ list.name }}</h4>
- <div class="todo_list">
- <div>{{ list.description|safe|linebreaks }}</div>
+ <div class="todo-list">
+ <a name="{{ list.id }}"></a>
+ <h4>{{ list.name }}</h4>
+ <p class="todo-info">{{ list.date_added|date }} - {{ list.creator.get_full_name }}</p>
+ <div>{{ list.description|urlize|linebreaks }}</div>
<table id="todo-pkglist-{{ list.id }}" class="results todo-table">
<thead>
<tr>
diff --git a/templates/todolists/view.html b/templates/todolists/view.html
index 477e0180..d4f5a08d 100644
--- a/templates/todolists/view.html
+++ b/templates/todolists/view.html
@@ -17,7 +17,9 @@
{% endif %}
</ul>
- <p>{{list.description|safe|linebreaks}}</p>
+ <p class="todo-info">{{ list.date_added|date }} - {{ list.creator.get_full_name }}</p>
+
+ <div>{{list.description|urlize|linebreaks}}</div>
<table id="dev-todo-pkglist" class="results todo-table">
<thead>
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 6bd456ae..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,14 +150,14 @@ 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),
- 'Arch Website Notification <nobody@archlinux.org>',
+ 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: