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