diff options
author | Parabola <dev@list.parabolagnulinux.org> | 2011-05-21 05:41:26 +0000 |
---|---|---|
committer | Parabola <dev@list.parabolagnulinux.org> | 2011-05-21 05:41:26 +0000 |
commit | 62059d83aea71ac7bde8902b20221e52c86a810b (patch) | |
tree | 758bd028d43a30893ba6261f39b1ebdbebbbf6e3 | |
parent | 081223981aa520f792757a1776588756a4107fd4 (diff) | |
parent | fd9ecb6eb1c8ee56adfbb58640d7a98baa6cd62c (diff) |
Merge branch 'master' of /srv/git/projects/parabolaweb
107 files changed, 4359 insertions, 693 deletions
@@ -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: @@ -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 %} › {{ 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 > 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&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> </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> </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">< Prev</a> {% else %} <span class="prev">< 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 ></a> {% else %} <span class="next">Next ></span> @@ -74,17 +82,17 @@ {% if perms.main.change_package %} <th> </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">< Prev</a> {% else %} <span class="prev">< 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 ></a> {% else %} <span class="next">Next ></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 <your-package-name></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 <your-package-name></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) @@ -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: |