From 5780cf2e217ce0a60d09cd2d4bbc8fa6e5c29689 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 26 Apr 2012 19:12:20 -0500 Subject: Tweak the PGP key visualization a bit * Add mouseover/mouseout events to highlight the incoming and outgoing signatures from a given key when moused over. * Change the border color of each developer key based on how many signatures from keys besides the master key they have. Thus, devs that have a lot of signatures from other devs will have a more green border; those with none will have a white border. Signed-off-by: Dan McGee --- sitestatic/archweb.css | 8 ------- visualize/static/visualize.js | 52 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index dcf0276a..3b638e20 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -1046,11 +1046,3 @@ ul.signoff-list { width: 100%; height: 100%; } - - #visualize-keys circle { - stroke-width: 1.5px; - } - - #visualize-keys line { - stroke: #888; - } diff --git a/visualize/static/visualize.js b/visualize/static/visualize.js index e73171ea..307c38c4 100644 --- a/visualize/static/visualize.js +++ b/visualize/static/visualize.js @@ -65,7 +65,9 @@ function packages_treemap(chart_id, orderings, default_order) { .style("display", function(d) { return d.children ? "none" : null; }) .html(cell_html); nodes.transition().duration(1500) - .style("background-color", function(d) { return d.children ? null : color(d[order.color_attr]); }) + .style("background-color", function(d) { + return d.children ? null : color(d[order.color_attr]); + }) .call(cell); nodes.exit().transition().duration(1500).remove(); }); @@ -133,9 +135,9 @@ function developer_keys(chart_id, data_url) { r = 10; var force = d3.layout.force() + .friction(0.5) .gravity(0.1) - .charge(-200) - .linkStrength(0.2) + .charge(-500) .size([jq_div.width(), jq_div.height()]); var svg = d3.select(chart_id) @@ -162,10 +164,13 @@ function developer_keys(chart_id, data_url) { return d.source >= 0 && d.target >= 0; }); - jQuery.map(json.nodes, function(d, i) { d.master_sigs = 0; }); + jQuery.map(json.nodes, function(d, i) { d.master_sigs = 0; d.other_sigs = 0; }); jQuery.map(edges, function(d, i) { + /* only the target gets credit in either case, as it is their key that was signed */ if (json.nodes[d.source].group === "master") { json.nodes[d.target].master_sigs += 1; + } else { + json.nodes[d.target].other_sigs += 1; } }); jQuery.map(json.nodes, function(d, i) { @@ -179,7 +184,11 @@ function developer_keys(chart_id, data_url) { var link = svg.selectAll("line") .data(edges) .enter() - .append("line"); + .append("line") + .style("stroke", "#888"); + + /* anyone with more than 7 - 1 == 6 signatures gets the top value */ + var stroke_color_scale = d3.scale.log().domain([1, 7]).range(["white", "green"]).clamp(true); var node = svg.selectAll("circle") .data(json.nodes) @@ -201,21 +210,47 @@ function developer_keys(chart_id, data_url) { if (d.approved === null) { return d3.rgb(fill(d.group)).darker(); } else if (d.approved) { - return "green"; + /* add 1 so we don't blow up the logarithm-based scale */ + return stroke_color_scale(d.other_sigs + 1); } else { return "red"; } }) + .style("stroke-width", "1.5px") .call(force.drag); node.append("title").text(function(d) { return d.name; }); + var nodeover = function(d, i) { + d3.select(this).transition().duration(500).style("stroke-width", "3px"); + link.filter(function(d_link, i) { + return d_link.source === d || d_link.target === d; + }).transition().duration(500).style("stroke", "#800"); + }; + var nodeout = function(d, i) { + d3.select(this).transition().duration(500).style("stroke-width", "1.5px"); + link.transition().duration(500).style("stroke", "#888"); + }; + + node.on("mouseover", nodeover) + .on("mouseout", nodeout); + var distance = function(d, i) { /* place a long line between all master keys and other keys. * however, other connected clusters should be close together. */ - if (d.source.group === "master" || d.target.group === "master") { + if (d.source.group === "master" || d.target.group === "master" || + d.source.group === "cacert" || d.target.group === "cacert") { return 200; } else { - return 50; + return 40; + } + }; + + var strength = function(d, i) { + if (d.source.group === "master" || d.target.group === "master" || + d.source.group === "cacert" || d.target.group === "cacert") { + return 0.2; + } else { + return 0.8; } }; @@ -235,6 +270,7 @@ function developer_keys(chart_id, data_url) { force.nodes(json.nodes) .links(edges) .linkDistance(distance) + .linkStrength(strength) .on("tick", tick) .start(); }); -- cgit v1.2.3-54-g00ecf From 3f150dcfade9443b3435309cb928f330966eb749 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 26 Apr 2012 22:19:02 -0500 Subject: Migrate news views to class-based generic views Signed-off-by: Dan McGee --- news/urls.py | 25 ++++++++---- news/views.py | 124 +++++++++++++++++++++++++++------------------------------- 2 files changed, 75 insertions(+), 74 deletions(-) diff --git a/news/urls.py b/news/urls.py index 10020f31..0eec6d86 100644 --- a/news/urls.py +++ b/news/urls.py @@ -1,14 +1,25 @@ from django.conf.urls import patterns +from django.contrib.auth.decorators import permission_required +from .views import (NewsDetailView, NewsListView, + NewsCreateView, NewsEditView, NewsDeleteView) + urlpatterns = patterns('news.views', - (r'^$', 'news_list', {}, 'news-list'), - (r'^add/$', 'add'), - (r'^preview/$', 'preview'), + (r'^$', + NewsListView.as_view(), {}, 'news-list'), + + (r'^preview/$', 'preview'), # old news URLs, permanent redirect view so we don't break all links - (r'^(?P\d+)/$', 'view_redirect'), - (r'^(?P[-\w]+)/$', 'view'), - (r'^(?P[-\w]+)/edit/$', 'edit'), - (r'^(?P[-\w]+)/delete/$', 'delete'), + (r'^(?P\d+)/$', 'view_redirect'), + + (r'^add/$', + permission_required('news.add_news')(NewsCreateView.as_view())), + (r'^(?P[-\w]+)/$', + NewsDetailView.as_view()), + (r'^(?P[-\w]+)/edit/$', + permission_required('news.change_news')(NewsEditView.as_view())), + (r'^(?P[-\w]+)/delete/$', + permission_required('news.delete_news')(NewsDeleteView.as_view())), ) # vim: set ts=4 sw=4 et: diff --git a/news/views.py b/news/views.py index 7ac009ba..268f0523 100644 --- a/news/views.py +++ b/news/views.py @@ -1,38 +1,15 @@ +import markdown + from django import forms -from django.contrib.auth.decorators import permission_required from django.http import HttpResponse from django.shortcuts import get_object_or_404, redirect from django.template.defaultfilters import slugify -from django.views.decorators.cache import never_cache -from django.views.generic import list_detail, create_update -from django.views.generic.simple import direct_to_template - -import markdown +from django.views.decorators.http import require_POST +from django.views.generic import (DetailView, ListView, + CreateView, UpdateView, DeleteView) from .models import News -def view_redirect(request, object_id): - newsitem = get_object_or_404(News, pk=object_id) - return redirect(newsitem, permanent=True) - -def view(request, slug=None): - return list_detail.object_detail(request, News.objects.all(), - slug=slug, - template_name="news/view.html", - template_object_name='news') - -#TODO: May as well use a date-based list here sometime -def news_list(request): - return list_detail.object_list(request, - News.objects.all().select_related('author').defer('content'), - paginate_by=50, - template_name="news/list.html", - template_object_name="news") - -class NewsForm(forms.ModelForm): - class Meta: - model = News - exclude = ('id', 'slug', 'author', 'postdate') def find_unique_slug(newsitem): '''Attempt to find a unique slug for this news item.''' @@ -46,46 +23,59 @@ def find_unique_slug(newsitem): return suffixed -@permission_required('news.add_news') -@never_cache -def add(request): - if request.POST: - form = NewsForm(request.POST) - if form.is_valid(): - newsitem = form.save(commit=False) - newsitem.author = request.user - newsitem.slug = find_unique_slug(newsitem) - newsitem.save() - return redirect(newsitem) - else: - form = NewsForm() - return direct_to_template(request, 'news/add.html', { 'form': form }) - -@permission_required('news.delete_news') -@never_cache -def delete(request, slug): - return create_update.delete_object(request, - News, - slug=slug, - post_delete_redirect='/news/', - template_name='news/delete.html', - template_object_name='news') - -@permission_required('news.change_news') -@never_cache -def edit(request, slug): - return create_update.update_object(request, - slug=slug, - form_class=NewsForm, - template_name="news/add.html") - -@permission_required('news.change_news') -@never_cache + +class NewsForm(forms.ModelForm): + class Meta: + model = News + exclude = ('id', 'slug', 'author', 'postdate') + + +class NewsDetailView(DetailView): + model = News + template_name = "news/view.html" + + +class NewsListView(ListView): + queryset = News.objects.all().select_related('author').defer('content') + template_name = "news/list.html" + paginate_by = 50 + + +class NewsCreateView(CreateView): + model = News + form_class = NewsForm + template_name = "news/add.html" + + def form_valid(self, form): + # special logic, we auto-fill the author and slug fields + newsitem = form.save(commit=False) + newsitem.author = self.request.user + newsitem.slug = find_unique_slug(newsitem) + newsitem.save() + return super(NewsCreateView, self).form_valid(form) + + +class NewsEditView(UpdateView): + model = News + form_class = NewsForm + template_name = "news/add.html" + + +class NewsDeleteView(DeleteView): + model = News + template_name = "news/delete.html" + success_url = "/news/" + + +def view_redirect(request, object_id): + newsitem = get_object_or_404(News, pk=object_id) + return redirect(newsitem, permanent=True) + + +@require_POST def preview(request): - markup = '' - if request.POST: - data = request.POST.get('data', '') - markup = markdown.markdown(data) + data = request.POST.get('data', '') + markup = markdown.markdown(data) return HttpResponse(markup) # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From dc94eade03022ce3a5286f5e781576321a5f1653 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 27 Apr 2012 08:59:00 -0500 Subject: Incomplete-only todolists optimization We can push this down to the database if we know in advance we only need the incomplete lists. This helps our call on the developer dashboard quite a bit; the time of the single query in question drops from >1300ms to around 40ms. Signed-off-by: Dan McGee --- devel/views.py | 3 +-- todolists/utils.py | 8 ++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/devel/views.py b/devel/views.py index d2ce65db..39f28a65 100644 --- a/devel/views.py +++ b/devel/views.py @@ -49,8 +49,7 @@ 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] + todolists = get_annotated_todolists(incomplete_only=True) signoffs = sorted(get_signoff_groups(user=request.user), key=operator.attrgetter('pkgbase')) diff --git a/todolists/utils.py b/todolists/utils.py index 24101e86..94f39f71 100644 --- a/todolists/utils.py +++ b/todolists/utils.py @@ -3,7 +3,7 @@ from main.models import Todolist -def get_annotated_todolists(): +def get_annotated_todolists(incomplete_only=False): qs = Todolist.objects.all() lists = qs.select_related('creator').defer( 'creator__email', 'creator__password', 'creator__is_staff', @@ -13,8 +13,12 @@ def get_annotated_todolists(): 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) + + if incomplete_only: + lists = lists.filter(id__in=lookup.keys()) + + # tag each list with an incomplete package count for todolist in lists: todolist.incomplete_count = lookup.get(todolist.id, 0) -- cgit v1.2.3-54-g00ecf From 15c94b2061de4ba618af977279fe2575db2ac0a5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 27 Apr 2012 09:02:18 -0500 Subject: Make sorting by time on clocks page more predictable This is a little bit of a hack but works well. Add the timezone name to the end of the sort so those in the same time zone end up next to each other. For timezones like CEST that have many different specifiers, it makes more sense to group them by 'Europe/Berlin', 'Europe/Paris', etc. Signed-off-by: Dan McGee --- sitestatic/archweb.css | 4 ++++ templates/devel/clock.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 3b638e20..cafae777 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -85,6 +85,10 @@ input[type=submit] { clear: both; } +.hide { + display: none; +} + hr { border: none; border-top: 1px solid #888; diff --git a/templates/devel/clock.html b/templates/devel/clock.html index 61e67c32..bdb7341d 100644 --- a/templates/devel/clock.html +++ b/templates/devel/clock.html @@ -34,7 +34,7 @@

Developer World Clocks

{{ dev.userprofile.alias }} {% if dev.userprofile.country %}{{ dev.userprofile.country.name }} {% endif %}{{ dev.userprofile.location }} {{ dev.userprofile.time_zone }} - {{ utc_now|timezone:dev.userprofile.time_zone|date:"Y-m-d H:i T" }} + {{ utc_now|timezone:dev.userprofile.time_zone|date:"Y-m-d H:i T" }} {{ dev.userprofile.time_zone }} {% endfor %} -- cgit v1.2.3-54-g00ecf From 57c1176d9e918c9d11ecd4a1e8bea3f138c8cf6a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 27 Apr 2012 09:04:12 -0500 Subject: Remove misleading comment from settings.py We're not using cache middleware anymore, and this bug is fixed anyway. Signed-off-by: Dan McGee --- settings.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/settings.py b/settings.py index 52339c53..50ed6c18 100644 --- a/settings.py +++ b/settings.py @@ -68,8 +68,6 @@ 'django.template.loaders.app_directories.Loader', ) -# This bug is a real bummer: -# http://code.djangoproject.com/ticket/14105 MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', -- cgit v1.2.3-54-g00ecf From 4e1e28729f97eb694cdcae2f3fe51b5178963069 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 27 Apr 2012 09:07:26 -0500 Subject: Use GenericIPAddressField in flag request ip_address New (and slightly odd with regards to verbose_name) in Django 1.4. This simply ensures a deployment in an IPv6 environment actually works as expected. If you were using PostgreSQL as a database backend, you won't be affected by this as the 'inet' type was already used, but at least now you can edit the values in the admin without getting an error. Signed-off-by: Dan McGee --- .../0014_auto__chg_field_flagrequest_ip_address.py | 181 +++++++++++++++++++++ packages/models.py | 4 +- 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 packages/migrations/0014_auto__chg_field_flagrequest_ip_address.py diff --git a/packages/migrations/0014_auto__chg_field_flagrequest_ip_address.py b/packages/migrations/0014_auto__chg_field_flagrequest_ip_address.py new file mode 100644 index 00000000..351b9985 --- /dev/null +++ b/packages/migrations/0014_auto__chg_field_flagrequest_ip_address.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.alter_column('packages_flagrequest', 'ip_address', self.gf('django.db.models.fields.GenericIPAddressField')(max_length=39)) + + def backwards(self, orm): + db.alter_column('packages_flagrequest', 'ip_address', self.gf('django.db.models.fields.IPAddressField')(max_length=15)) + + 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',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + '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': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'", 'on_delete': 'models.PROTECT', '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': '2'}), + '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.flagrequest': { + 'Meta': {'object_name': 'FlagRequest'}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'is_legitimate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'num_packages': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + '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', 'db_index': 'True'}), + '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'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index 820e61ba..7a7a81cd 100644 --- a/packages/models.py +++ b/packages/models.py @@ -173,7 +173,9 @@ class FlagRequest(models.Model): user = models.ForeignKey(User, blank=True, null=True) user_email = models.EmailField('email address') created = models.DateTimeField(editable=False) - ip_address = models.IPAddressField('IP address') + # Great work, Django... https://code.djangoproject.com/ticket/18212 + ip_address = models.GenericIPAddressField(verbose_name='IP address', + unpack_ipv4=True) pkgbase = models.CharField(max_length=255, db_index=True) version = models.CharField(max_length=255, default='') repo = models.ForeignKey(Repo) -- cgit v1.2.3-54-g00ecf From 80e7d19726a95b40727b7f35b9ad80b436b14b93 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 27 Apr 2012 09:24:34 -0500 Subject: Dev dashboard performance improvement Rather than one query per cell in the arches and repos statistics tables, we can group these together up front using Django annotations. This means we only need one query per table. In my local instance with all of the staging repos imported, this reduces the total query count on this page from 56 to 26, a rather marked improvement. Signed-off-by: Dan McGee --- devel/views.py | 15 ++++++++++----- templates/devel/index.html | 8 ++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/devel/views.py b/devel/views.py index 39f28a65..cf0d8ad2 100644 --- a/devel/views.py +++ b/devel/views.py @@ -13,7 +13,7 @@ from django.contrib.sites.models import Site from django.core.mail import send_mail from django.db import transaction -from django.db.models import F +from django.db.models import F, Count from django.http import Http404 from django.shortcuts import get_object_or_404 from django.template import loader, Context @@ -54,6 +54,11 @@ def index(request): signoffs = sorted(get_signoff_groups(user=request.user), key=operator.attrgetter('pkgbase')) + arches = Arch.objects.all().annotate( + total_ct=Count('packages'), flagged_ct=Count('packages__flag_date')) + repos = Repo.objects.all().annotate( + total_ct=Count('packages'), flagged_ct=Count('packages__flag_date')) + maintainers = get_annotated_maintainers() maintained = PackageRelation.objects.filter( @@ -70,12 +75,12 @@ def index(request): page_dict = { 'todos': todolists, - 'repos': Repo.objects.all(), - 'arches': Arch.objects.all(), + 'arches': arches, + 'repos': repos, 'maintainers': maintainers, 'orphan': orphan, - 'flagged' : flagged, - 'todopkgs' : todopkgs, + 'flagged': flagged, + 'todopkgs': todopkgs, 'signoffs': signoffs } diff --git a/templates/devel/index.html b/templates/devel/index.html index a0ccb91f..c67cf277 100644 --- a/templates/devel/index.html +++ b/templates/devel/index.html @@ -194,10 +194,10 @@

Stats by Architecture

{{ arch.name }} - {{ arch.packages.count }} packages + {{ arch.total_ct }} packages - {{ arch.packages.flagged.count }} packages + {{ arch.flagged_ct }} packages {% endfor %} @@ -224,10 +224,10 @@

Stats by Repository

{{ repo.name }} - {{ repo.packages.count }} packages + {{ repo.total_ct }} packages - {{ repo.packages.flagged.count }} packages + {{ repo.flagged_ct }} packages {% endfor %} -- cgit v1.2.3-54-g00ecf From baaa14dfc06cf8c381363883ebf4fd805b348e34 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 27 Apr 2012 09:57:08 -0500 Subject: Flip package update feeds table orientation Most of the time, more rows is better than more columns, and there are more repositories than architectures. Signed-off-by: Dan McGee --- templates/public/feeds.html | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/templates/public/feeds.html b/templates/public/feeds.html index 0f80e1c2..b4214b1b 100644 --- a/templates/public/feeds.html +++ b/templates/public/feeds.html @@ -35,19 +35,23 @@

Package Feeds

- - - {% for repo in repos %} - + + {% for arch in arches %} + {% endfor %} - {% for arch in arches %} - + + {% for arch in arches %} - {% for repo in repos %} + {% endfor %} + + {% for repo in repos %} + + + {% for arch in arches %} {% endfor %} -- cgit v1.2.3-54-g00ecf From 8d21e9f4b1d5a31880f9973d758de9becc90eb39 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 28 Apr 2012 18:25:37 -0500 Subject: mirrorresolv: only run update query if values changed 98% of the time, we won't need to update the existing values as it will be the same as the prior run of this command. Do a quick check of the old and new values and don't send anything to the database if there is no need for an update. Signed-off-by: Dan McGee --- mirrors/management/commands/mirrorresolv.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mirrors/management/commands/mirrorresolv.py b/mirrors/management/commands/mirrorresolv.py index 4e812f2d..0370f8ed 100644 --- a/mirrors/management/commands/mirrorresolv.py +++ b/mirrors/management/commands/mirrorresolv.py @@ -41,13 +41,19 @@ def resolve_mirrors(): logger.debug("requesting list of mirror URLs") for mirrorurl in MirrorUrl.objects.filter(mirror__active=True): try: + # save old values, we can skip no-op updates this way + oldvals = (mirrorurl.has_ipv4, mirrorurl.has_ipv6) logger.debug("resolving %3i (%s)", mirrorurl.id, mirrorurl.hostname) families = mirrorurl.address_families() mirrorurl.has_ipv4 = socket.AF_INET in families mirrorurl.has_ipv6 = socket.AF_INET6 in families logger.debug("%s: v4: %s v6: %s", mirrorurl.hostname, mirrorurl.has_ipv4, mirrorurl.has_ipv6) - mirrorurl.save(force_update=True) + # now check new values, only update if new != old + newvals = (mirrorurl.has_ipv4, mirrorurl.has_ipv6) + if newvals != oldvals: + logger.debug("values changed for %s", mirrorurl) + mirrorurl.save(force_update=True) except socket.error, e: logger.warn("error resolving %s: %s", mirrorurl.hostname, e) -- cgit v1.2.3-54-g00ecf From 942f73deba9b6e13da8b4e801ddd98bee3e0b90d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 29 Apr 2012 19:00:41 -0500 Subject: Make colspan match number of columns in developer dashboard Signed-off-by: Dan McGee --- templates/devel/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/devel/index.html b/templates/devel/index.html index c67cf277..d1bd4d2c 100644 --- a/templates/devel/index.html +++ b/templates/devel/index.html @@ -39,7 +39,7 @@

My Flagged Packages

{% empty %} - + {% endfor %}
ArchitectureAll Repos{{ repo }}{{ arch }}
{{ arch }}All ReposFeed
{{ repo }}Feed
{{ pkg.last_update|date }}
No flagged packages to display
No flagged packages to display
-- cgit v1.2.3-54-g00ecf From 25a2fbc7c1cb50fa80ed4de50830721507765a91 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 29 Apr 2012 19:15:19 -0500 Subject: Add a "last action" column to developer clocks page This allows people to easily see if a developer has done anything recently that we can easily grab a date for. Obviously this doesn't include all sources of activity, so the list of things checked is clearly stated at the top. Signed-off-by: Dan McGee --- devel/views.py | 43 ++++++++++++++++++++++++++++++++++++------- templates/devel/clock.html | 21 ++++++++++++++++++--- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/devel/views.py b/devel/views.py index cf0d8ad2..85acda74 100644 --- a/devel/views.py +++ b/devel/views.py @@ -1,4 +1,4 @@ -from datetime import date, datetime, timedelta +from datetime import timedelta import operator import pytz import random @@ -9,24 +9,28 @@ from django.http import HttpResponseRedirect from django.contrib.auth.decorators import \ login_required, permission_required, user_passes_test +from django.contrib.admin.models import LogEntry, ADDITION from django.contrib.auth.models import User, Group +from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site from django.core.mail import send_mail from django.db import transaction -from django.db.models import F, Count +from django.db.models import F, Count, Max 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 django.utils.encoding import force_unicode from django.utils.http import http_date from .models import UserProfile from main.models import Package, PackageDepend, PackageFile, TodolistPkg from main.models import Arch, Repo from main.utils import utc_now -from packages.models import PackageRelation +from news.models import News +from packages.models import PackageRelation, Signoff from packages.utils import get_signoff_groups from todolists.utils import get_annotated_todolists from .utils import get_annotated_maintainers, UserFinder @@ -91,6 +95,33 @@ def clock(request): devs = User.objects.filter(is_active=True).order_by( 'first_name', 'last_name').select_related('userprofile') + latest_news = dict(News.objects.filter( + author__is_active=True).values_list('author').order_by( + ).annotate(last_post=Max('postdate'))) + latest_package = dict(Package.objects.filter( + packager__is_active=True).values_list('packager').order_by( + ).annotate(last_build=Max('build_date'))) + latest_signoff = dict(Signoff.objects.filter( + user__is_active=True).values_list('user').order_by( + ).annotate(last_signoff=Max('created'))) + latest_log = dict(LogEntry.objects.filter( + user__is_active=True).values_list('user').order_by( + ).annotate(last_log=Max('action_time'))) + + for dev in devs: + dates = [ + latest_news.get(dev.id, None), + latest_package.get(dev.id, None), + latest_signoff.get(dev.id, None), + latest_log.get(dev.id, None), + dev.last_login, + ] + dates = [d for d in dates if d is not None] + if dates: + dev.last_action = max(dates) + else: + dev.last_action = None + now = utc_now() page_dict = { 'developers': devs, @@ -135,7 +166,8 @@ class Meta: def change_profile(request): if request.POST: form = ProfileForm(request.POST) - profile_form = UserProfileForm(request.POST, request.FILES, instance=request.user.get_profile()) + profile_form = UserProfileForm(request.POST, request.FILES, + instance=request.user.get_profile()) if form.is_valid() and profile_form.is_valid(): request.user.email = form.cleaned_data['email'] if form.cleaned_data['passwd1']: @@ -331,9 +363,6 @@ def save(self, commit=True): 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, diff --git a/templates/devel/clock.html b/templates/devel/clock.html index bdb7341d..6a3f0a69 100644 --- a/templates/devel/clock.html +++ b/templates/devel/clock.html @@ -11,8 +11,18 @@

Developer World Clocks

This page helps prevent you from waking a sleeping developer. It also depends on developers keeping the time zone information up to date, so if you see 'UTC' listed, pester them to update their settings.

+

The "Last Action" column shows the last time this developer has done + something we know about. Considered dates for each developer include:

+
    +
  • Build date of a package in the repositories
  • +
  • Last login to the developer side of this site
  • +
  • Admin log entry on this site (e.g., adding a donor, modifying a + mirror)
  • +
  • Post date of a news item
  • +
  • Signing off on a package
  • +

- UTC Time: {{ utc_now|date:"Y-m-d H:i T" }} + Current UTC Time: {{ utc_now|date:"Y-m-d H:i T" }}

@@ -21,6 +31,7 @@

Developer World Clocks

+ @@ -32,6 +43,7 @@

Developer World Clocks

+ @@ -45,8 +57,11 @@

Developer World Clocks

{% endblock %} -- cgit v1.2.3-54-g00ecf From 44eb2d5ee0fa9e1b495027cec3e663ff85c0ed1d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 29 Apr 2012 21:26:23 -0500 Subject: Use a custom User-Agent when checking mirror URLs Signed-off-by: Dan McGee --- mirrors/management/commands/mirrorcheck.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/mirrors/management/commands/mirrorcheck.py b/mirrors/management/commands/mirrorcheck.py index 7ffb7773..c1269226 100644 --- a/mirrors/management/commands/mirrorcheck.py +++ b/mirrors/management/commands/mirrorcheck.py @@ -35,6 +35,7 @@ stream=sys.stderr) logger = logging.getLogger() + class Command(NoArgsCommand): help = "Runs a check on all known mirror URLs to determine their up-to-date status." @@ -49,13 +50,16 @@ def handle_noargs(self, **options): return check_current_mirrors() + def check_mirror_url(mirror_url): url = mirror_url.url + 'lastsync' logger.info("checking URL %s", url) log = MirrorLog(url=mirror_url, check_time=utc_now()) + headers = {'User-Agent': 'archweb/1.0'} + req = urllib2.Request(url, None, headers) try: start = time.time() - result = urllib2.urlopen(url, timeout=10) + result = urllib2.urlopen(req, timeout=10) data = result.read() result.close() end = time.time() @@ -104,6 +108,7 @@ def check_mirror_url(mirror_url): return log + def mirror_url_worker(work, output): while True: try: @@ -116,11 +121,12 @@ def mirror_url_worker(work, output): except Empty: return 0 + class MirrorCheckPool(object): - def __init__(self, work, num_threads=10): + def __init__(self, urls, num_threads=10): self.tasks = Queue() self.logs = deque() - for i in list(work): + for i in list(urls): self.tasks.put(i) self.threads = [] for i in range(num_threads): @@ -140,6 +146,7 @@ def run(self): MirrorLog.objects.bulk_create(self.logs) logger.debug("log entries saved") + def check_current_mirrors(): urls = MirrorUrl.objects.filter( protocol__is_download=True, @@ -149,8 +156,4 @@ def check_current_mirrors(): pool.run() return 0 -# For lack of a better place to put it, here is a query to get latest check -# result joined with mirror details: -# SELECT mu.*, m.*, ml.* FROM mirrors_mirrorurl mu JOIN mirrors_mirror m ON mu.mirror_id = m.id JOIN mirrors_mirrorlog ml ON mu.id = ml.url_id LEFT JOIN mirrors_mirrorlog ml2 ON ml.url_id = ml2.url_id AND ml.id < ml2.id WHERE ml2.id IS NULL AND m.active = 1 AND m.public = 1; - # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From f5c00577b3d364527481e5c98aa0fce8e300a341 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 1 May 2012 16:32:54 -0500 Subject: Refresh apple-touch-icon logos Rerun optipng on them to shrink them a bit more, and add the 'TM' text to the 114x114 icon since it is big enough to hold it and still be visible. Signed-off-by: Dan McGee --- sitestatic/logos/apple-touch-icon-114x114.png | Bin 3240 -> 2088 bytes sitestatic/logos/apple-touch-icon-57x57.png | Bin 1638 -> 1173 bytes sitestatic/logos/apple-touch-icon-72x72.png | Bin 2076 -> 1437 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/sitestatic/logos/apple-touch-icon-114x114.png b/sitestatic/logos/apple-touch-icon-114x114.png index e6365ee2..a5b4282a 100644 Binary files a/sitestatic/logos/apple-touch-icon-114x114.png and b/sitestatic/logos/apple-touch-icon-114x114.png differ diff --git a/sitestatic/logos/apple-touch-icon-57x57.png b/sitestatic/logos/apple-touch-icon-57x57.png index d2d78262..d7d592c7 100644 Binary files a/sitestatic/logos/apple-touch-icon-57x57.png and b/sitestatic/logos/apple-touch-icon-57x57.png differ diff --git a/sitestatic/logos/apple-touch-icon-72x72.png b/sitestatic/logos/apple-touch-icon-72x72.png index 170656e0..5983885f 100644 Binary files a/sitestatic/logos/apple-touch-icon-72x72.png and b/sitestatic/logos/apple-touch-icon-72x72.png differ -- cgit v1.2.3-54-g00ecf From b06efbe343eb6cd5cafe4c121d53a538302f9156 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 1 May 2012 16:33:58 -0500 Subject: Add a 144x144 apple-touch-icon to the resources This is used on the high-resolution display of newer iPads, as well as other tablets sure to come with high-resolution displays. Signed-off-by: Dan McGee --- sitestatic/logos/apple-touch-icon-144x144.png | Bin 0 -> 3063 bytes templates/base.html | 1 + 2 files changed, 1 insertion(+) create mode 100644 sitestatic/logos/apple-touch-icon-144x144.png diff --git a/sitestatic/logos/apple-touch-icon-144x144.png b/sitestatic/logos/apple-touch-icon-144x144.png new file mode 100644 index 00000000..cd177f2c Binary files /dev/null and b/sitestatic/logos/apple-touch-icon-144x144.png differ diff --git a/templates/base.html b/templates/base.html index b8a5be46..e6ccc972 100644 --- a/templates/base.html +++ b/templates/base.html @@ -10,6 +10,7 @@ + {% block head %}{% endblock %} -- cgit v1.2.3-54-g00ecf From b59e79f3878d59b83c6867eb5c6196f8f003dcd9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 1 May 2012 17:13:33 -0500 Subject: Opensearch enhancements * Add a 64x64 icon as indicated in the Opensearch specification. * Add suggestions capability and a new view providing suggestions based on package name starting with the typed value. Signed-off-by: Dan McGee --- packages/views/__init__.py | 18 +++++++++++++++++- sitestatic/logos/icon-transparent-64x64.png | Bin 0 -> 1430 bytes templates/packages/opensearch.xml | 17 +++++++++++------ urls.py | 2 ++ 4 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 sitestatic/logos/icon-transparent-64x64.png diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 6a9c5275..21d17470 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -7,7 +7,7 @@ from django.http import HttpResponse, Http404 from django.shortcuts import get_object_or_404, redirect from django.utils import simplejson -from django.views.decorators.http import require_POST +from django.views.decorators.http import require_GET, require_POST from django.views.decorators.vary import vary_on_headers from django.views.generic.simple import direct_to_template @@ -24,6 +24,7 @@ from .signoff import signoffs, signoff_package, signoff_options, signoffs_json +@require_GET def opensearch(request): if request.is_secure(): domain = "https://%s" % request.META['HTTP_HOST'] @@ -34,6 +35,21 @@ def opensearch(request): {'domain': domain}, mimetype='application/opensearchdescription+xml') + +@require_GET +def opensearch_suggest(request): + search_term = request.GET.get('q', '') + if search_term == '': + return HttpResponse('', mimetype='application/x-suggestions+json') + + names = Package.objects.filter( + pkgname__startswith=search_term).values_list( + 'pkgname', flat=True).order_by('pkgname').distinct()[:10] + results = [search_term, list(names)] + to_json = simplejson.dumps(results, ensure_ascii=False) + return HttpResponse(to_json, mimetype='application/x-suggestions+json') + + @permission_required('main.change_package') @require_POST def update(request): diff --git a/sitestatic/logos/icon-transparent-64x64.png b/sitestatic/logos/icon-transparent-64x64.png new file mode 100644 index 00000000..0318f183 Binary files /dev/null and b/sitestatic/logos/icon-transparent-64x64.png differ diff --git a/templates/packages/opensearch.xml b/templates/packages/opensearch.xml index 216be3e9..6c50d991 100644 --- a/templates/packages/opensearch.xml +++ b/templates/packages/opensearch.xml @@ -1,13 +1,18 @@ - +{% load static from staticfiles %} - Arch Linux Packages - Search the Arch Linux package repositories. + Arch Packages + Arch Linux Package Repository Search + Search the Arch Linux package repositories by keyword in package names and descriptions. linux archlinux package software - {{domain}}/static/favicon.ico + {{ domain }}{% static "favicon.ico" %} + {{ domain }}{% static "logos/icon-transparent-64x64.png" %} en-us UTF-8 UTF-8 - - + + + diff --git a/urls.py b/urls.py index 0f2e07af..c0ce8c6f 100644 --- a/urls.py +++ b/urls.py @@ -91,6 +91,8 @@ (r'^visualize/', include('visualize.urls')), (r'^opensearch/packages/$', 'packages.views.opensearch', {}, 'opensearch-packages'), + (r'^opensearch/packages/suggest$', 'packages.views.opensearch_suggest', + {}, 'opensearch-packages-suggest'), (r'^todolists/$','todolists.views.public_list'), ) -- cgit v1.2.3-54-g00ecf From f3e0adcb2fc9a26e2ad9337a47550a37590074d9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 1 May 2012 18:12:50 -0500 Subject: Add some caching to the Opensearch-related views Both some simple cache headers as well as low-level results caching on search terms suggestions. Signed-off-by: Dan McGee --- packages/views/__init__.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 21d17470..0f1dc799 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -4,9 +4,11 @@ from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import User +from django.core.cache import cache from django.http import HttpResponse, Http404 from django.shortcuts import get_object_or_404, redirect from django.utils import simplejson +from django.views.decorators.cache import cache_control from django.views.decorators.http import require_GET, require_POST from django.views.decorators.vary import vary_on_headers from django.views.generic.simple import direct_to_template @@ -25,6 +27,7 @@ @require_GET +@cache_control(public=True, max_age=86400) def opensearch(request): if request.is_secure(): domain = "https://%s" % request.META['HTTP_HOST'] @@ -37,16 +40,21 @@ def opensearch(request): @require_GET +@cache_control(public=True, max_age=300) def opensearch_suggest(request): search_term = request.GET.get('q', '') if search_term == '': return HttpResponse('', mimetype='application/x-suggestions+json') - names = Package.objects.filter( - pkgname__startswith=search_term).values_list( - 'pkgname', flat=True).order_by('pkgname').distinct()[:10] - results = [search_term, list(names)] - to_json = simplejson.dumps(results, ensure_ascii=False) + cache_key = 'opensearch:packages:' + search_term + to_json = cache.get(cache_key, None) + if to_json is None: + names = Package.objects.filter( + pkgname__startswith=search_term).values_list( + 'pkgname', flat=True).order_by('pkgname').distinct()[:10] + results = [search_term, list(names)] + to_json = simplejson.dumps(results, ensure_ascii=False) + cache.set('opensearch:packages:%s' % search_term, to_json, 300) return HttpResponse(to_json, mimetype='application/x-suggestions+json') -- cgit v1.2.3-54-g00ecf From badc535aeb1d310a9b8aa59aade07045e6eae653 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 18 Apr 2012 15:05:43 -0500 Subject: Ensure order_by default value is cleared when using distinct() Otherwise the queryset returns nonsensical results. I find the design of this less than obvious but so be it; we can ensure the results work regardless of a default ordering on the model. Signed-off-by: Dan McGee --- devel/views.py | 6 ++++-- main/models.py | 7 ++++--- news/views.py | 3 ++- packages/utils.py | 3 ++- todolists/views.py | 4 ++-- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/devel/views.py b/devel/views.py index 85acda74..7ef33362 100644 --- a/devel/views.py +++ b/devel/views.py @@ -249,7 +249,8 @@ def report(request, report_name, username=None): if username: pkg_ids = set(packages.values_list('id', flat=True)) bad_files = bad_files.filter(pkg__in=pkg_ids) - bad_files = bad_files.values_list('pkg_id', flat=True).distinct() + bad_files = bad_files.values_list( + 'pkg_id', flat=True).order_by().distinct() packages = packages.filter(id__in=set(bad_files)) elif report_name == 'uncompressed-info': title = 'Packages with uncompressed infopages' @@ -260,7 +261,8 @@ def report(request, report_name, username=None): if username: pkg_ids = set(packages.values_list('id', flat=True)) bad_files = bad_files.filter(pkg__in=pkg_ids) - bad_files = bad_files.values_list('pkg_id', flat=True).distinct() + bad_files = bad_files.values_list( + 'pkg_id', flat=True).order_by().distinct() packages = packages.filter(id__in=set(bad_files)) elif report_name == 'unneeded-orphans': title = 'Orphan packages required by no other packages' diff --git a/main/models.py b/main/models.py index c532ed56..398cb88b 100644 --- a/main/models.py +++ b/main/models.py @@ -13,7 +13,7 @@ class TodolistManager(models.Manager): def incomplete(self): - return self.filter(todolistpkg__complete=False).distinct() + return self.filter(todolistpkg__complete=False).order_by().distinct() class PackageManager(models.Manager): def flagged(self): @@ -378,7 +378,7 @@ def get_providers(self, arches=None, testing=None, staging=None): '''Return providers of this dep. Does *not* include exact matches as it checks the Provision names only, use get_best_satisfier() instead.''' pkgs = Package.objects.normal().filter( - provides__name=self.depname).distinct() + provides__name=self.depname).order_by().distinct() if arches is not None: pkgs = pkgs.filter(arch__in=arches) @@ -432,7 +432,8 @@ def packages(self): @property def package_names(self): # depends on packages property returning a queryset - return self.packages.values_list('pkg__pkgname', flat=True).distinct() + return self.packages.values_list( + 'pkg__pkgname', flat=True).order_by().distinct() class Meta: db_table = 'todolists' diff --git a/news/views.py b/news/views.py index 268f0523..03f3b0ac 100644 --- a/news/views.py +++ b/news/views.py @@ -13,7 +13,8 @@ def find_unique_slug(newsitem): '''Attempt to find a unique slug for this news item.''' - existing = list(News.objects.values_list('slug', flat=True).distinct()) + existing = list(News.objects.values_list( + 'slug', flat=True).order_by().distinct()) suffixed = slug = slugify(newsitem.title) suffix = 0 diff --git a/packages/utils.py b/packages/utils.py index a3c13b17..8d00bd68 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -218,7 +218,8 @@ def attach_maintainers(packages): packages = list(packages) pkgbases = set(p.pkgbase for p in packages) rels = PackageRelation.objects.filter(type=PackageRelation.MAINTAINER, - pkgbase__in=pkgbases).values_list('pkgbase', 'user_id').distinct() + pkgbase__in=pkgbases).values_list( + 'pkgbase', 'user_id').order_by().distinct() # get all the user objects we will need user_ids = set(rel[1] for rel in rels) diff --git a/todolists/views.py b/todolists/views.py index e5cc0823..70209b6d 100644 --- a/todolists/views.py +++ b/todolists/views.py @@ -49,8 +49,8 @@ def flag(request, list_id, pkg_id): @login_required def view(request, list_id): todolist = get_object_or_404(Todolist, id=list_id) - svn_roots = Repo.objects.order_by().values_list( - 'svn_root', flat=True).distinct() + svn_roots = Repo.objects.values_list( + 'svn_root', flat=True).order_by().distinct() # we don't hold onto the result, but the objects are the same here, # so accessing maintainers in the template is now cheap attach_maintainers(tp.pkg for tp in todolist.packages) -- cgit v1.2.3-54-g00ecf From a03cb4ba1994296bff4a0237439144b0b3d8f01b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 2 May 2012 09:38:10 -0500 Subject: Move release version scope up a bit on the page Signed-off-by: Dan McGee --- templates/public/download.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/public/download.html b/templates/public/download.html index f57e565a..6c9ccf11 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -7,11 +7,11 @@ {% block navbarclass %}anb-download{% endblock %} {% block content %} +{% with "2011.08.19" as version %}

Arch Linux Downloads

- {% with "2011.08.19" as version %}

Release Info

All available images can be burned to a CD, mounted as an ISO file, @@ -164,10 +164,10 @@

Checksums

{% endfor %}
{% endcache %} - {% endwith %}

If you want to become an Official Arch Linux Mirror please follow the instructions listed here.

+{% endwith %} {% endblock %} -- cgit v1.2.3-54-g00ecf From cf8ecdf9fce0573ad207d024708f21a5dbbbb120 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 2 May 2012 09:46:52 -0500 Subject: rematch_developers: do mass updates instead of single saves When updating a lot of objects, it makes much more sense to perform targeted update queries rather than one-row-at-a-time saves. Signed-off-by: Dan McGee --- devel/management/commands/rematch_developers.py | 61 ++++++++++++------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/devel/management/commands/rematch_developers.py b/devel/management/commands/rematch_developers.py index 8383cc8d..ab2f0f4b 100644 --- a/devel/management/commands/rematch_developers.py +++ b/devel/management/commands/rematch_developers.py @@ -46,52 +46,51 @@ def handle_noargs(self, **options): @transaction.commit_on_success def match_packager(finder): - logger.info("getting all unmatched packages") + logger.info("getting all unmatched packager strings") package_count = matched_count = 0 - unknown = set() - - for package in Package.objects.filter(packager__isnull=True): - if package.packager_str in unknown: - continue - logger.debug("package %s, packager string %s", - package.pkgname, package.packager_str) - package_count += 1 - user = finder.find(package.packager_str) + mapping = {} + + unmatched = Package.objects.filter(packager__isnull=True).values_list( + 'packager_str', flat=True).order_by().distinct() + + for packager in unmatched: + logger.debug("packager string %s", packager) + user = finder.find(packager) if user: - package.packager = user + mapping[packager] = user logger.debug(" found user %s" % user.username) - package.save() matched_count += 1 - else: - unknown.add(package.packager_str) - logger.info("%d packager strings checked, %d newly matched", + for packager_str, user in mapping.items(): + package_count += Package.objects.filter(packager__isnull=True, + packager_str=packager_str).update(packager=user) + + logger.info("%d packages updated, %d packager strings matched", package_count, matched_count) - logger.debug("unknown packagers:\n%s", - "\n".join(unknown)) @transaction.commit_on_success def match_flagrequest(finder): - logger.info("getting all non-user flag requests") + logger.info("getting all flag requests emails from unknown users") req_count = matched_count = 0 - unknown = set() - - for request in FlagRequest.objects.filter(user__isnull=True): - if request.user_email in unknown: - continue - logger.debug("email %s", request.user_email) - req_count += 1 - user = finder.find_by_email(request.user_email) + mapping = {} + + unmatched = FlagRequest.objects.filter(user__isnull=True).values_list( + 'user_email', flat=True).order_by().distinct() + + for user_email in unmatched: + logger.debug("email %s", user_email) + user = finder.find_by_email(user_email) if user: - request.user = user + mapping[user_email] = user logger.debug(" found user %s" % user.username) - request.save() matched_count += 1 - else: - unknown.add(request.user_email) - logger.info("%d request emails checked, %d newly matched", + for user_email, user in mapping.items(): + req_count += FlagRequest.objects.filter(user__isnull=True, + user_email=user_email).update(user=user) + + logger.info("%d request emails updated, %d emails matched", req_count, matched_count) # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 768bc688aab844cf9fdf9809b9381aaf0042f2fc Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 2 May 2012 10:21:30 -0500 Subject: Flagging related cleanups and improvements Touch up the style slightly on the flag help popup to match the main site style more closely. When a logged-in user is flagging a package out of date, we have no need for them to fill in the email field since we already have an email address on file. Signed-off-by: Dan McGee --- packages/views/flag.py | 34 ++++++++++++++++++++++++---------- templates/packages/flaghelp.html | 20 +++++++++----------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/packages/views/flag.py b/packages/views/flag.py index 0d2f9009..7fa2d508 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -6,16 +6,13 @@ from django.shortcuts import get_object_or_404, redirect from django.template import loader, Context from django.views.generic.simple import direct_to_template -from django.views.decorators.cache import never_cache +from django.views.decorators.cache import cache_page, never_cache from ..models import FlagRequest from main.models import Package from main.utils import utc_now -def flaghelp(request): - return direct_to_template(request, 'packages/flaghelp.html') - class FlagForm(forms.Form): email = forms.EmailField(label='E-mail Address') message = forms.CharField(label='Message To Developer', @@ -26,6 +23,20 @@ class FlagForm(forms.Form): widget=forms.TextInput(attrs={'style': 'display:none;'}), required=False) + def __init__(self, *args, **kwargs): + # we remove the 'email' field if this form is being shown to a + # logged-in user, e.g., a developer. + auth = kwargs.pop('authenticated', False) + super(FlagForm, self).__init__(*args, **kwargs) + if auth: + del self.fields['email'] + + +@cache_page(3600) +def flaghelp(request): + return direct_to_template(request, 'packages/flaghelp.html') + + @never_cache def flag(request, name, repo, arch): pkg = get_object_or_404(Package, @@ -41,8 +52,10 @@ def flag(request, name, repo, arch): repo__staging=pkg.repo.staging).order_by( 'pkgname', 'repo__name', 'arch__name') + authenticated = request.user.is_authenticated() + if request.POST: - form = FlagForm(request.POST) + form = FlagForm(request.POST, authenticated=authenticated) if form.is_valid() and form.cleaned_data['website'] == '': # save the package list for later use flagged_pkgs = list(pkgs) @@ -54,9 +67,12 @@ def flag(request, name, repo, arch): else: version = '' - email = form.cleaned_data['email'] message = form.cleaned_data['message'] ip_addr = request.META.get('REMOTE_ADDR') + if authenticated: + email = request.user.email + else: + email = form.cleaned_data['email'] @transaction.commit_on_success def perform_updates(): @@ -68,7 +84,7 @@ def perform_updates(): ip_address=ip_addr, pkgbase=pkg.pkgbase, version=version, repo=pkg.repo, num_packages=len(flagged_pkgs)) - if request.user.is_authenticated(): + if authenticated: flag_request.user = request.user flag_request.save() @@ -106,9 +122,7 @@ def perform_updates(): arch=arch) else: initial = {} - if request.user.is_authenticated(): - initial['email'] = request.user.email - form = FlagForm(initial=initial) + form = FlagForm(authenticated=authenticated) context = { 'package': pkg, diff --git a/templates/packages/flaghelp.html b/templates/packages/flaghelp.html index 819a2f01..399b7e01 100644 --- a/templates/packages/flaghelp.html +++ b/templates/packages/flaghelp.html @@ -1,20 +1,19 @@ - +{% load static from staticfiles %} Flagging Packages - -

Flagging Packages

-

If you notice that a package is out-of-date (i.e., there is a newer stable release available), then please notify us by using the Flag button in the Package Details @@ -29,8 +28,7 @@

Flagging Packages

with your additional text.

Note: Please do not use this facility if the - package is broken! Use the bugtracker instead.

- -- cgit v1.2.3-54-g00ecf From d2d0895f13835569ff25a3161ddb94cd655dfd4f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 2 May 2012 12:23:21 -0500 Subject: Allow mirrorlist generator pattern to match any protocol Add a helper method that checks if we know about the protocol; if so, we can spit out a URL for it. This allows (if you are insane) generation of an rsync mirrorlist, for instance. Signed-off-by: Dan McGee --- mirrors/urls_mirrorlist.py | 5 +---- mirrors/views.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/mirrors/urls_mirrorlist.py b/mirrors/urls_mirrorlist.py index e0f44c78..1444eca9 100644 --- a/mirrors/urls_mirrorlist.py +++ b/mirrors/urls_mirrorlist.py @@ -3,10 +3,7 @@ urlpatterns = patterns('mirrors.views', (r'^$', 'generate_mirrorlist', {}, 'mirrorlist'), (r'^all/$', 'find_mirrors', {'countries': ['all']}), - (r'^all/ftp/$', 'find_mirrors', - {'countries': ['all'], 'protocols': ['ftp']}), - (r'^all/http/$', 'find_mirrors', - {'countries': ['all'], 'protocols': ['http']}), + (r'^all/(?P[A-z]+)/$', 'find_mirrors_simple') ) # vim: set ts=4 sw=4 et: diff --git a/mirrors/views.py b/mirrors/views.py index c52656f7..6f37ace1 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -82,12 +82,15 @@ def generate_mirrorlist(request): def find_mirrors(request, countries=None, protocols=None, use_status=False, ipv4_supported=True, ipv6_supported=True): if not protocols: - protocols = MirrorProtocol.objects.filter( - is_download=True).values_list('protocol', flat=True) + protocols = MirrorProtocol.objects.filter(is_download=True) + elif hasattr(protocols, 'model') and protocols.model == MirrorProtocol: + # we already have a queryset, no need to query again + pass + else: + protocols = MirrorProtocol.objects.filter(protocol__in=protocols) qset = MirrorUrl.objects.select_related().filter( - protocol__protocol__in=protocols, - mirror__public=True, mirror__active=True, - ) + protocol__in=protocols, + mirror__public=True, mirror__active=True) if countries and 'all' not in countries: qset = qset.filter(Q(country__in=countries) | Q(mirror__country__in=countries)) @@ -124,6 +127,11 @@ def find_mirrors(request, countries=None, protocols=None, use_status=False, mimetype='text/plain') +def find_mirrors_simple(request, protocol): + proto = get_object_or_404(MirrorProtocol, protocol=protocol) + return find_mirrors(request, protocols=[proto]) + + def mirrors(request): mirror_list = Mirror.objects.select_related().order_by('tier', 'country') if not request.user.is_authenticated(): -- cgit v1.2.3-54-g00ecf From 12bf4c1b1e7df2d934b9dfde8629137dedeea99f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 18 Feb 2012 19:17:00 -0600 Subject: Add a smart protocol filter to mirrorlist generator This will only list FTP mirrors for a given country if there are no HTTP mirrors available, since FTP must die. Signed-off-by: Dan McGee --- mirrors/views.py | 55 ++++++++++++++++++++++-------- templates/mirrors/mirrorlist_generate.html | 1 + 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/mirrors/views.py b/mirrors/views.py index 6f37ace1..eac78ff2 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -1,4 +1,5 @@ from datetime import timedelta +from itertools import groupby from operator import attrgetter, itemgetter from django import forms @@ -79,8 +80,34 @@ def generate_mirrorlist(request): {'mirrorlist_form': form}) +def default_protocol_filter(original_urls): + key_func = attrgetter('real_country') + sorted_urls = sorted(original_urls, key=key_func) + urls = [] + for _, group in groupby(sorted_urls, key=key_func): + cntry_urls = list(group) + if any(url.protocol.default for url in cntry_urls): + cntry_urls = [url for url in cntry_urls if url.protocol.default] + urls.extend(cntry_urls) + return urls + + +def status_filter(original_urls): + status_info = get_mirror_statuses() + scores = dict((u.id, u.score) for u in status_info['urls']) + urls = [] + for u in original_urls: + u.score = scores.get(u.id, None) + # also include mirrors that don't have an up to date score + # (as opposed to those that have been set with no score) + if (u.id not in scores) or (u.score and u.score < 100.0): + urls.append(u) + # if a url doesn't have a score, treat it as the highest possible + return sorted(urls, key=lambda x: x.score or 100.0) + + def find_mirrors(request, countries=None, protocols=None, use_status=False, - ipv4_supported=True, ipv6_supported=True): + ipv4_supported=True, ipv6_supported=True, smart_protocol=False): if not protocols: protocols = MirrorProtocol.objects.filter(is_download=True) elif hasattr(protocols, 'model') and protocols.model == MirrorProtocol: @@ -102,23 +129,17 @@ def find_mirrors(request, countries=None, protocols=None, use_status=False, ip_version |= Q(has_ipv6=True) qset = qset.filter(ip_version) + if smart_protocol: + urls = default_protocol_filter(qset) + else: + urls = qset + if not use_status: - urls = qset.order_by('mirror__name', 'url') - urls = sorted(urls, key=attrgetter('real_country')) + sort_key = attrgetter('real_country', 'mirror.name', 'url') + urls = sorted(urls, key=sort_key) template = 'mirrors/mirrorlist.txt' else: - status_info = get_mirror_statuses() - scores = dict([(u.id, u.score) for u in status_info['urls']]) - urls = [] - for u in qset: - u.score = scores.get(u.id, None) - # also include mirrors that don't have an up to date score - # (as opposed to those that have been set with no score) - if (u.id not in scores) or \ - (u.score and u.score < 100.0): - urls.append(u) - # if a url doesn't have a score, treat it as the highest possible - urls = sorted(urls, key=lambda x: x.score or 100.0) + urls = status_filter(urls) template = 'mirrors/mirrorlist_status.txt' return direct_to_template(request, template, { @@ -128,6 +149,10 @@ def find_mirrors(request, countries=None, protocols=None, use_status=False, def find_mirrors_simple(request, protocol): + if protocol == 'smart': + # generate a 'smart' mirrorlist, one that only includes FTP mirrors if + # no HTTP mirror is available in that country. + return find_mirrors(request, smart_protocol=True) proto = get_object_or_404(MirrorProtocol, protocol=protocol) return find_mirrors(request, protocols=[proto]) diff --git a/templates/mirrors/mirrorlist_generate.html b/templates/mirrors/mirrorlist_generate.html index e6f5e28c..34bda63d 100644 --- a/templates/mirrors/mirrorlist_generate.html +++ b/templates/mirrors/mirrorlist_generate.html @@ -23,6 +23,7 @@

Mirrorlist with all available mirrors

  • All mirrors
  • All mirrors, FTP only
  • All mirrors, HTTP only
  • +
  • All mirrors, Smart protocols: this link only includes FTP mirrors if an HTTP mirror is not available in a given country.
  • Customized by country mirrorlist

    -- cgit v1.2.3-54-g00ecf From 86f8efaeb1f67138c194d0c373f9d91e2999c5dd Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 4 May 2012 10:57:59 -0500 Subject: Fix search suggestions for invalid cache keys Unfortunately, "invalid" in this case includes spaces, which is a bit crazy. MD5 the provided search term before using it as a cache key to be safe. Signed-off-by: Dan McGee --- packages/views/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 0f1dc799..559368b9 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -1,3 +1,4 @@ +import hashlib from string import Template from urllib import urlencode @@ -46,7 +47,7 @@ def opensearch_suggest(request): if search_term == '': return HttpResponse('', mimetype='application/x-suggestions+json') - cache_key = 'opensearch:packages:' + search_term + cache_key = 'opensearch:packages:' + hashlib.md5(search_term).hexdigest() to_json = cache.get(cache_key, None) if to_json is None: names = Package.objects.filter( @@ -54,7 +55,7 @@ def opensearch_suggest(request): 'pkgname', flat=True).order_by('pkgname').distinct()[:10] results = [search_term, list(names)] to_json = simplejson.dumps(results, ensure_ascii=False) - cache.set('opensearch:packages:%s' % search_term, to_json, 300) + cache.set(cache_key, to_json, 300) return HttpResponse(to_json, mimetype='application/x-suggestions+json') -- cgit v1.2.3-54-g00ecf From d4c7a48623f90cdc508d1824bf47ce3e398dd820 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 5 May 2012 10:19:49 -0500 Subject: Fix suggestion caching again for non-ASCII characters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is why you should test this stuff with random input before rolling it out. Whoops. URL that caught this problem: /opensearch/packages/suggest?q=%D7%A0%D7%9F%D7%92%D7%9F aka /opensearch/packages/suggest?q=נןגן Signed-off-by: Dan McGee --- packages/views/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 559368b9..60c3a46b 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -47,7 +47,8 @@ def opensearch_suggest(request): if search_term == '': return HttpResponse('', mimetype='application/x-suggestions+json') - cache_key = 'opensearch:packages:' + hashlib.md5(search_term).hexdigest() + cache_key = 'opensearch:packages:' + \ + hashlib.md5(search_term.encode('utf-8')).hexdigest() to_json = cache.get(cache_key, None) if to_json is None: names = Package.objects.filter( -- cgit v1.2.3-54-g00ecf From 5a5f667885827f5a22f9994b5f4897e63bc07178 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 5 May 2012 18:32:00 -0500 Subject: Update SevenL sponsor verbiage Signed-off-by: Dan McGee --- templates/public/donate.html | 22 ++++++++++++---------- templates/public/index.html | 6 +++--- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/templates/public/donate.html b/templates/public/donate.html index 3b3ee108..0809e2a6 100644 --- a/templates/public/donate.html +++ b/templates/public/donate.html @@ -44,17 +44,19 @@

    Commercial sponsors and contributions

    title="velocity network"> -

    We also wish to extend a special Thank You to SevenL Networks +

    We also wish to extend a special thank you to 7L Networks for their generous and ongoing contribution of a dedicated Arch Linux - server. You too can have a dedicated Arch Linux server hosted by SevenL... - head over to their website for more details.

    - - + server. You too can have a dedicated Arch Linux server, or your server colocation service, + with 7L. Head over to their website for more details.

    + +

    More thanks go to AirVM.com for contributing a VMWare-based Virtual Machine.

    diff --git a/templates/public/index.html b/templates/public/index.html index de77b47f..32e25f98 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -209,11 +209,11 @@

    More Resources

    Velocity Network - It's about time - - We would like to express our thanks to SevenL Networks for their generous contribution + + 7L Networks Inc. - AirVM.com - Your Green Technology Partner + AirVM.com - Your Green Technology Partner -- cgit v1.2.3-54-g00ecf From c8a2b8db5188e04266744db9f5acc7d23d3c2e0e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 11 May 2012 23:07:17 -0500 Subject: Update requirements.txt Bump to a new South bugfix release as well as bumping the pgpdump library requirement to a more performant version. Signed-off-by: Dan McGee --- requirements.txt | 4 ++-- requirements_prod.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3efc73c2..146ccbcc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Django==1.4 Markdown==2.1.1 -South==0.7.4 +South==0.7.5 django-countries==1.2 -pgpdump==1.1 +pgpdump==1.3 pytz>=2012c diff --git a/requirements_prod.txt b/requirements_prod.txt index 39b9f734..2f0dec50 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -1,8 +1,8 @@ Django==1.4 Markdown==2.1.1 -South==0.7.4 +South==0.7.5 django-countries==1.2 -pgpdump==1.1 +pgpdump==1.3 psycopg2==2.4.5 pyinotify==0.9.3 python-memcached==1.48 -- cgit v1.2.3-54-g00ecf From f36d876aca5571f09032d0d2a67c8b1f1a3258c8 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 12 May 2012 09:22:52 -0500 Subject: Change to new time access methods in pgpdump code Signed-off-by: Dan McGee --- devel/views.py | 2 +- templates/packages/details.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/devel/views.py b/devel/views.py index 7ef33362..0f1c8d15 100644 --- a/devel/views.py +++ b/devel/views.py @@ -280,7 +280,7 @@ def report(request, report_name, username=None): filtered = [] packages = packages.filter(pgp_signature__isnull=False) for package in packages: - sig_date = package.signature.datetime.replace(tzinfo=pytz.utc) + sig_date = package.signature.creation_time.replace(tzinfo=pytz.utc) package.sig_date = sig_date.date() key_id = package.signature.key_id signer = finder.find_by_pgp_key(key_id) diff --git a/templates/packages/details.html b/templates/packages/details.html index 4ab55ef2..4cb6032e 100644 --- a/templates/packages/details.html +++ b/templates/packages/details.html @@ -165,7 +165,7 @@

    Versions Elsewhere

    - + {% else %} -- cgit v1.2.3-54-g00ecf From a5f5557493446bede78adb0584c88208234f874e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 12 May 2012 09:32:30 -0500 Subject: Use python json module directly in place of simplejson As of Python 2.6, this is a builtin module that has all the same functions and capabilities of the Django simplejson module. Additionally simplejson is deprecated in the upcoming Django 1.5 release. Signed-off-by: Dan McGee --- mirrors/views.py | 5 ++--- packages/views/__init__.py | 10 ++++------ packages/views/search.py | 5 ++--- packages/views/signoff.py | 7 +++---- todolists/views.py | 6 +++--- visualize/views.py | 8 ++++---- 6 files changed, 18 insertions(+), 23 deletions(-) diff --git a/mirrors/views.py b/mirrors/views.py index eac78ff2..b0be6238 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -1,5 +1,6 @@ from datetime import timedelta from itertools import groupby +import json from operator import attrgetter, itemgetter from django import forms @@ -10,7 +11,6 @@ from django.shortcuts import get_object_or_404 from django.views.decorators.csrf import csrf_exempt from django.views.generic.simple import direct_to_template -from django.utils import simplejson from django_countries.countries import COUNTRIES from .models import Mirror, MirrorUrl, MirrorProtocol @@ -237,8 +237,7 @@ def status_json(request): status_info = get_mirror_statuses() data = status_info.copy() data['version'] = 3 - to_json = simplejson.dumps(data, ensure_ascii=False, - cls=MirrorStatusJSONEncoder) + to_json = json.dumps(data, ensure_ascii=False, cls=MirrorStatusJSONEncoder) response = HttpResponse(to_json, mimetype='application/json') return response diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 60c3a46b..c1a035d0 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -1,4 +1,5 @@ import hashlib +import json from string import Template from urllib import urlencode @@ -8,7 +9,6 @@ from django.core.cache import cache from django.http import HttpResponse, Http404 from django.shortcuts import get_object_or_404, redirect -from django.utils import simplejson from django.views.decorators.cache import cache_control from django.views.decorators.http import require_GET, require_POST from django.views.decorators.vary import vary_on_headers @@ -55,7 +55,7 @@ def opensearch_suggest(request): pkgname__startswith=search_term).values_list( 'pkgname', flat=True).order_by('pkgname').distinct()[:10] results = [search_term, list(names)] - to_json = simplejson.dumps(results, ensure_ascii=False) + to_json = json.dumps(results, ensure_ascii=False) cache.set(cache_key, to_json, 300) return HttpResponse(to_json, mimetype='application/x-suggestions+json') @@ -197,8 +197,7 @@ def files(request, name, repo, arch): def details_json(request, name, repo, arch): pkg = get_object_or_404(Package, pkgname=name, repo__name__iexact=repo, arch__name=arch) - to_json = simplejson.dumps(pkg, ensure_ascii=False, - cls=PackageJSONEncoder) + to_json = json.dumps(pkg, ensure_ascii=False, cls=PackageJSONEncoder) return HttpResponse(to_json, mimetype='application/json') def files_json(request, name, repo, arch): @@ -212,8 +211,7 @@ def files_json(request, name, repo, arch): 'arch': pkg.arch.name.lower(), 'files': fileslist, } - to_json = simplejson.dumps(data, ensure_ascii=False, - cls=PackageJSONEncoder) + to_json = json.dumps(data, ensure_ascii=False, cls=PackageJSONEncoder) return HttpResponse(to_json, mimetype='application/json') def download(request, name, repo, arch): diff --git a/packages/views/search.py b/packages/views/search.py index a09de0a7..a89822be 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -1,4 +1,5 @@ from datetime import datetime +import json from django import forms from django.contrib.admin.widgets import AdminDateWidget @@ -6,7 +7,6 @@ from django.db.models import Q from django.http import HttpResponse from django.views.generic import list_detail -from django.utils import simplejson from main.models import Package, Arch, Repo from main.utils import make_choice @@ -179,8 +179,7 @@ def search_json(request): container['results'] = packages container['valid'] = True - to_json = simplejson.dumps(container, ensure_ascii=False, - cls=PackageJSONEncoder) + to_json = json.dumps(container, ensure_ascii=False, cls=PackageJSONEncoder) return HttpResponse(to_json, mimetype='application/json') # vim: set ts=4 sw=4 et: diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 63341a1d..61d949fc 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -1,3 +1,4 @@ +import json from operator import attrgetter from django import forms @@ -7,7 +8,6 @@ from django.db import transaction from django.http import HttpResponse, Http404 from django.shortcuts import get_list_or_404, redirect, render -from django.utils import simplejson from django.views.decorators.cache import never_cache from django.views.generic.simple import direct_to_template @@ -67,7 +67,7 @@ def signoff_package(request, name, repo, arch, revoke=False): 'known_bad': spec.known_bad, 'user': str(request.user), } - return HttpResponse(simplejson.dumps(data, ensure_ascii=False), + return HttpResponse(json.dumps(data, ensure_ascii=False), mimetype='application/json') return redirect('package-signoffs') @@ -183,8 +183,7 @@ def signoffs_json(request): 'version': 2, 'signoff_groups': signoff_groups, } - to_json = simplejson.dumps(data, ensure_ascii=False, - cls=SignoffJSONEncoder) + to_json = json.dumps(data, ensure_ascii=False, cls=SignoffJSONEncoder) response = HttpResponse(to_json, mimetype='application/json') return response diff --git a/todolists/views.py b/todolists/views.py index 70209b6d..b9ba0903 100644 --- a/todolists/views.py +++ b/todolists/views.py @@ -1,5 +1,6 @@ -from django import forms +import json +from django import forms from django.http import HttpResponse from django.core.mail import send_mail from django.shortcuts import get_list_or_404, get_object_or_404, redirect @@ -9,7 +10,6 @@ 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, Repo from packages.utils import attach_maintainers @@ -42,7 +42,7 @@ def flag(request, list_id, pkg_id): pkg.save() if request.is_ajax(): return HttpResponse( - simplejson.dumps({'complete': pkg.complete}), + json.dumps({'complete': pkg.complete}), mimetype='application/json') return redirect(todolist) diff --git a/visualize/views.py b/visualize/views.py index afc5429d..95f8c314 100644 --- a/visualize/views.py +++ b/visualize/views.py @@ -1,9 +1,9 @@ from datetime import datetime +import json from django.contrib.auth.models import User from django.db.models import Count, Sum, Q from django.http import HttpResponse -from django.utils import simplejson from django.views.decorators.cache import cache_page from django.views.generic.simple import direct_to_template @@ -61,13 +61,13 @@ def build_map(name, arch, repo): @cache_page(1800) def by_arch(request): data = arch_repo_data() - to_json = simplejson.dumps(data['by_arch'], ensure_ascii=False) + to_json = json.dumps(data['by_arch'], ensure_ascii=False) return HttpResponse(to_json, mimetype='application/json') @cache_page(1800) def by_repo(request): data = arch_repo_data() - to_json = simplejson.dumps(data['by_repo'], ensure_ascii=False) + to_json = json.dumps(data['by_repo'], ensure_ascii=False) return HttpResponse(to_json, mimetype='application/json') @@ -109,7 +109,7 @@ def pgp_keys(request): data = { 'nodes': node_list, 'edges': edge_list } - to_json = simplejson.dumps(data, ensure_ascii=False) + to_json = json.dumps(data, ensure_ascii=False) return HttpResponse(to_json, mimetype='application/json') # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 76516cae45f3d1080065608bdb8f2d086322012f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 13 May 2012 19:45:57 -0500 Subject: Don't limit protocols returned by mirror status function If results weren't available for certain URLs, they won't show up anyway in this list, and if we start to check rsync URLs, then we want their values to come back in this status list. Signed-off-by: Dan McGee --- mirrors/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mirrors/utils.py b/mirrors/utils.py index 32fa3587..54de567e 100644 --- a/mirrors/utils.py +++ b/mirrors/utils.py @@ -32,11 +32,9 @@ def annotate_url(url, delays): @cache_function(123) def get_mirror_statuses(cutoff=default_cutoff): cutoff_time = utc_now() - cutoff - protocols = list(MirrorProtocol.objects.filter(is_download=True)) # I swear, this actually has decent performance... urls = MirrorUrl.objects.select_related('mirror', 'protocol').filter( mirror__active=True, mirror__public=True, - protocol__in=protocols, logs__check_time__gte=cutoff_time).annotate( check_count=Count('logs'), success_count=Count('logs__duration'), -- cgit v1.2.3-54-g00ecf From a570c2c7fa404c2eb1760c6fb428047083c0c957 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 13 May 2012 20:10:46 -0500 Subject: Change mirror log error text to unlimited length Use TextField rather than a limited-length CharField; leave it up to the code filling this field to determine an appropriate length. Signed-off-by: Dan McGee --- .../0017_auto__chg_field_mirrorlog_error.py | 66 ++++++++++++++++++++++ mirrors/models.py | 6 +- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 mirrors/migrations/0017_auto__chg_field_mirrorlog_error.py diff --git a/mirrors/migrations/0017_auto__chg_field_mirrorlog_error.py b/mirrors/migrations/0017_auto__chg_field_mirrorlog_error.py new file mode 100644 index 00000000..2f76c099 --- /dev/null +++ b/mirrors/migrations/0017_auto__chg_field_mirrorlog_error.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.alter_column('mirrors_mirrorlog', 'error', self.gf('django.db.models.fields.TextField')()) + + def backwards(self, orm): + db.alter_column('mirrors_mirrorlog', 'error', self.gf('django.db.models.fields.CharField')(max_length=255)) + + 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_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', 'blank': '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', 'on_delete': 'models.SET_NULL'}) + }, + '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.TextField', [], {'default': "''", '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'}, + 'default': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + '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_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', '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'", 'on_delete': 'models.PROTECT', '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 19437610..8c2bd7fc 100644 --- a/mirrors/models.py +++ b/mirrors/models.py @@ -44,6 +44,7 @@ def downstream(self): def get_absolute_url(self): return '/mirrors/%s/' % self.name + class MirrorProtocol(models.Model): protocol = models.CharField(max_length=10, unique=True) is_download = models.BooleanField(default=True, @@ -57,6 +58,7 @@ def __unicode__(self): class Meta: ordering = ('protocol',) + class MirrorUrl(models.Model): url = models.CharField("URL", max_length=255, unique=True) protocol = models.ForeignKey(MirrorProtocol, related_name="urls", @@ -104,6 +106,7 @@ def __unicode__(self): class Meta: verbose_name = 'mirror URL' + class MirrorRsync(models.Model): ip = models.CharField("IP", max_length=24) mirror = models.ForeignKey(Mirror, related_name="rsync_ips") @@ -114,13 +117,14 @@ def __unicode__(self): class Meta: verbose_name = 'mirror rsync IP' + class MirrorLog(models.Model): url = models.ForeignKey(MirrorUrl, related_name="logs") check_time = models.DateTimeField(db_index=True) last_sync = models.DateTimeField(null=True) duration = models.FloatField(null=True) is_success = models.BooleanField(default=True) - error = models.CharField(max_length=255, blank=True, default='') + error = models.TextField(blank=True, default='') def __unicode__(self): return "Check of %s at %s" % (self.url.url, self.check_time) -- cgit v1.2.3-54-g00ecf From 3b0b9012353d5ffda564998cab58f986770361be Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 13 May 2012 20:14:16 -0500 Subject: Use linebreaksbr filter on log error message text If we get a multi-line message in, we should show line breaks at the appropriate places. Signed-off-by: Dan McGee --- templates/mirrors/status.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/mirrors/status.html b/templates/mirrors/status.html index 8ee1d46e..34896c07 100644 --- a/templates/mirrors/status.html +++ b/templates/mirrors/status.html @@ -89,7 +89,7 @@

    Mirror Syncing Error Log

    - + -- cgit v1.2.3-54-g00ecf From 2f7d770b261b3428bcff366ba6ff4fa631dd980a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 13 May 2012 20:15:00 -0500 Subject: Add rsync support to mirrorcheck and other small improvements The main changes in this patch implement rsync:// protocol checking support by calling the rsync binary, requested in FS#29878. We track and log much of the same things as we already do for FTP and HTTP URLs- check time, last sync, total check duration, etc. Also added in this patch is a configurable timeout value which defaults to the previous hardcoded value of 10 seconds; this can be passed as an option to the mirrorcheck command. Signed-off-by: Dan McGee --- mirrors/management/commands/mirrorcheck.py | 130 ++++++++++++++++++++--------- templates/mirrors/status.html | 4 +- 2 files changed, 91 insertions(+), 43 deletions(-) diff --git a/mirrors/management/commands/mirrorcheck.py b/mirrors/management/commands/mirrorcheck.py index c1269226..ae89d5e0 100644 --- a/mirrors/management/commands/mirrorcheck.py +++ b/mirrors/management/commands/mirrorcheck.py @@ -9,22 +9,26 @@ Usage: ./manage.py mirrorcheck """ -from django.core.management.base import NoArgsCommand -from django.db import transaction - from collections import deque from datetime import datetime import logging +import os +from optparse import make_option +from pytz import utc import re import socket +import subprocess import sys import time +import tempfile from threading import Thread import types -from pytz import utc from Queue import Queue, Empty import urllib2 +from django.core.management.base import NoArgsCommand +from django.db import transaction + from main.utils import utc_now from mirrors.models import MirrorUrl, MirrorLog @@ -37,10 +41,15 @@ class Command(NoArgsCommand): + option_list = NoArgsCommand.option_list + ( + make_option('-t', '--timeout', dest='timeout', default='10', + help='Timeout value for connecting to URL'), + ) help = "Runs a check on all known mirror URLs to determine their up-to-date status." def handle_noargs(self, **options): v = int(options.get('verbosity', 0)) + timeout = int(options.get('timeout', 10)) if v == 0: logger.level = logging.ERROR elif v == 1: @@ -48,10 +57,29 @@ def handle_noargs(self, **options): elif v == 2: logger.level = logging.DEBUG - return check_current_mirrors() + urls = MirrorUrl.objects.select_related('protocol').filter( + mirror__active=True, mirror__public=True) + + pool = MirrorCheckPool(urls, timeout) + pool.run() + return 0 -def check_mirror_url(mirror_url): +def parse_lastsync(log, data): + '''lastsync file should be an epoch value created by us.''' + try: + parsed_time = datetime.utcfromtimestamp(int(data)) + log.last_sync = parsed_time.replace(tzinfo=utc) + except ValueError: + # it is bad news to try logging the lastsync value; + # sometimes we get a crazy-encoded web page. + # if we couldn't parse a time, this is a failure. + log.last_sync = None + log.error = "Could not parse time from lastsync" + log.is_success = False + + +def check_mirror_url(mirror_url, timeout): url = mirror_url.url + 'lastsync' logger.info("checking URL %s", url) log = MirrorLog(url=mirror_url, check_time=utc_now()) @@ -59,28 +87,14 @@ def check_mirror_url(mirror_url): req = urllib2.Request(url, None, headers) try: start = time.time() - result = urllib2.urlopen(req, timeout=10) + result = urllib2.urlopen(req, timeout=timeout) data = result.read() result.close() end = time.time() - # lastsync should be an epoch value created by us - parsed_time = None - try: - parsed_time = datetime.utcfromtimestamp(int(data)) - parsed_time = parsed_time.replace(tzinfo=utc) - except ValueError: - # it is bad news to try logging the lastsync value; - # sometimes we get a crazy-encoded web page. - pass - - log.last_sync = parsed_time - # if we couldn't parse a time, this is a failure - if parsed_time is None: - log.error = "Could not parse time from lastsync" - log.is_success = False + parse_lastsync(log, data) log.duration = end - start logger.debug("success: %s, %.2f", url, log.duration) - except urllib2.HTTPError, e: + except urllib2.HTTPError as e: if e.code == 404: # we have a duration, just not a success end = time.time() @@ -88,7 +102,7 @@ def check_mirror_url(mirror_url): log.is_success = False log.error = str(e) logger.debug("failed: %s, %s", url, log.error) - except urllib2.URLError, e: + except urllib2.URLError as e: log.is_success = False log.error = e.reason if isinstance(e.reason, types.StringTypes) and \ @@ -101,20 +115,64 @@ def check_mirror_url(mirror_url): elif isinstance(e.reason, socket.error): log.error = e.reason.args[1] logger.debug("failed: %s, %s", url, log.error) - except socket.timeout, e: + except socket.timeout as e: log.is_success = False log.error = "Connection timed out." logger.debug("failed: %s, %s", url, log.error) + except socket.error as e: + log.is_success = False + log.error = str(e) + logger.debug("failed: %s, %s", url, log.error) + + return log + + +def check_rsync_url(mirror_url, timeout): + url = mirror_url.url + 'lastsync' + logger.info("checking URL %s", url) + log = MirrorLog(url=mirror_url, check_time=utc_now()) + + tempdir = tempfile.mkdtemp() + lastsync_path = os.path.join(tempdir, 'lastsync') + rsync_cmd = ["rsync", "--quiet", "--contimeout=%d" % timeout, + "--timeout=%d" % timeout, url, lastsync_path] + try: + with open(os.devnull, 'w') as devnull: + proc = subprocess.Popen(rsync_cmd, stdout=devnull, + stderr=subprocess.PIPE) + start = time.time() + _, errdata = proc.communicate() + end = time.time() + log.duration = end - start + if proc.returncode != 0: + logger.debug("error: %s, %s", url, errdata) + log.is_success = False + log.error = errdata.strip() + # look at rsync error code- if we had a command error or timed out, + # don't record a duration as it is misleading + if proc.returncode in (1, 30, 35): + log.duration = None + else: + logger.debug("success: %s, %.2f", url, log.duration) + with open(lastsync_path, 'r') as lastsync: + parse_lastsync(log, lastsync.read()) + finally: + if os.path.exists(lastsync_path): + os.unlink(lastsync_path) + os.rmdir(tempdir) return log -def mirror_url_worker(work, output): +def mirror_url_worker(work, output, timeout): while True: try: - item = work.get(block=False) + url = work.get(block=False) try: - log = check_mirror_url(item) + if url.protocol.protocol == 'rsync': + log = check_rsync_url(url, timeout) + else: + log = check_mirror_url(url, timeout) output.append(log) finally: work.task_done() @@ -123,7 +181,7 @@ def mirror_url_worker(work, output): class MirrorCheckPool(object): - def __init__(self, urls, num_threads=10): + def __init__(self, urls, timeout=10, num_threads=10): self.tasks = Queue() self.logs = deque() for i in list(urls): @@ -131,7 +189,7 @@ def __init__(self, urls, num_threads=10): self.threads = [] for i in range(num_threads): thread = Thread(target=mirror_url_worker, - args=(self.tasks, self.logs)) + args=(self.tasks, self.logs, timeout)) thread.daemon = True self.threads.append(thread) @@ -142,18 +200,8 @@ def run(self): thread.start() logger.debug("joining on all threads") self.tasks.join() - logger.debug("processing log entries") + logger.debug("processing %d log entries", len(self.logs)) MirrorLog.objects.bulk_create(self.logs) logger.debug("log entries saved") - -def check_current_mirrors(): - urls = MirrorUrl.objects.filter( - protocol__is_download=True, - mirror__active=True, mirror__public=True) - - pool = MirrorCheckPool(urls) - pool.run() - return 0 - # vim: set ts=4 sw=4 et: diff --git a/templates/mirrors/status.html b/templates/mirrors/status.html index 34896c07..2c350f56 100644 --- a/templates/mirrors/status.html +++ b/templates/mirrors/status.html @@ -16,8 +16,8 @@

    Mirror Status

    has synced recently. This page contains several pieces of information about each mirror.

      -
    • Mirror URL: Mirrors are checked on a per-URL basis. If - both FTP and HTTP access are provided, both will be listed here.
    • +
    • Mirror URL: Mirrors are checked on a per-URL basis. All + available URLs and protocols for each known mirror are listed.
    • Completion %: The number of mirror checks that have successfully connected and disconnected from the given URL. If this is below 100%, the mirror may be unreliable.
    • -- cgit v1.2.3-54-g00ecf From ae1c526ffbe908322f0dd8d8805360b81ab22b0f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 13 May 2012 20:35:50 -0500 Subject: Add ability to restrict status report to single tier This should make it easier to catch errors in our Tier 1 mirrors. Signed-off-by: Dan McGee --- mirrors/urls.py | 1 + mirrors/utils.py | 2 +- mirrors/views.py | 19 ++++++++++++++++--- templates/mirrors/status.html | 4 ++-- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/mirrors/urls.py b/mirrors/urls.py index f002e9d6..bb4eb969 100644 --- a/mirrors/urls.py +++ b/mirrors/urls.py @@ -4,6 +4,7 @@ (r'^$', 'mirrors', {}, 'mirror-list'), (r'^status/$', 'status', {}, 'mirror-status'), (r'^status/json/$', 'status_json', {}, 'mirror-status-json'), + (r'^status/tier/(?P\d+)/$', 'status', {}, 'mirror-status-tier'), (r'^(?P[\.\-\w]+)/$', 'mirror_details'), ) diff --git a/mirrors/utils.py b/mirrors/utils.py index 54de567e..2014411d 100644 --- a/mirrors/utils.py +++ b/mirrors/utils.py @@ -88,7 +88,7 @@ def get_mirror_errors(cutoff=default_cutoff): is_success=False, check_time__gte=cutoff_time, url__mirror__active=True, url__mirror__public=True).values( 'url__url', 'url__country', 'url__protocol__protocol', - 'url__mirror__country', 'error').annotate( + 'url__mirror__country', 'url__mirror__tier', 'error').annotate( error_count=Count('error'), last_occurred=Max('check_time') ).order_by('-last_occurred', '-error_count') errors = list(errors) diff --git a/mirrors/views.py b/mirrors/views.py index b0be6238..8f092be7 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -13,7 +13,7 @@ from django.views.generic.simple import direct_to_template from django_countries.countries import COUNTRIES -from .models import Mirror, MirrorUrl, MirrorProtocol +from .models import Mirror, MirrorUrl, MirrorProtocol, TIER_CHOICES from .utils import get_mirror_statuses, get_mirror_errors COUNTRY_LOOKUP = dict(COUNTRIES) @@ -184,7 +184,11 @@ def mirror_details(request, name): {'mirror': mirror, 'urls': all_urls}) -def status(request): +def status(request, tier=None): + if tier is not None: + tier = int(tier) + if tier not in [t[0] for t in TIER_CHOICES]: + raise Http404 bad_timedelta = timedelta(days=3) status_info = get_mirror_statuses() @@ -192,17 +196,26 @@ def status(request): good_urls = [] bad_urls = [] for url in urls: + # screen by tier if we were asked to + if tier is not None and url.mirror.tier != tier: + continue # split them into good and bad lists based on delay if not url.delay or url.delay > bad_timedelta: bad_urls.append(url) else: good_urls.append(url) + error_logs = get_mirror_errors() + if tier is not None: + error_logs = [log for log in error_logs + if log['url__mirror__tier'] == tier] + context = status_info.copy() context.update({ 'good_urls': sorted(good_urls, key=attrgetter('score')), 'bad_urls': sorted(bad_urls, key=lambda u: u.delay or timedelta.max), - 'error_logs': get_mirror_errors(), + 'error_logs': error_logs, + 'tier': tier, }) return direct_to_template(request, 'mirrors/status.html', context) diff --git a/templates/mirrors/status.html b/templates/mirrors/status.html index 2c350f56..472e9501 100644 --- a/templates/mirrors/status.html +++ b/templates/mirrors/status.html @@ -2,11 +2,11 @@ {% load static from staticfiles %} {% load mirror_status %} -{% block title %}Arch Linux - Mirror Status{% endblock %} +{% block title %}Arch Linux - Mirror Status{% if tier != None %} - Tier {{ tier }}{% endif %}{% endblock %} {% block content %}
      -

      Mirror Status

      +

      Mirror Status{% if tier != None %} - Tier {{ tier }}{% endif %}

      This page reports the status of all known, public, and active Arch Linux mirrors. All data on this page reflects the status of the mirrors within the last {{ cutoff|hours }}. All listed times are UTC. The check script runs -- cgit v1.2.3-54-g00ecf From 17e33f9118e9749b1e3fdfd76686e85dbcecfb00 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 May 2012 19:17:30 -0500 Subject: Refactor an abstract base class out of conflicts/provides/replaces Signed-off-by: Dan McGee --- packages/models.py | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/packages/models.py b/packages/models.py index 7a7a81cd..f57c9f3c 100644 --- a/packages/models.py +++ b/packages/models.py @@ -218,10 +218,10 @@ def __unicode__(self): class Meta: ordering = ['name'] -class Conflict(models.Model): - pkg = models.ForeignKey('main.Package', related_name='conflicts') + +class RelatedToBase(models.Model): + '''A base class for conflicts/provides/replaces/etc.''' name = models.CharField(max_length=255, db_index=True) - comparison = models.CharField(max_length=255, default='') version = models.CharField(max_length=255, default='') def __unicode__(self): @@ -230,36 +230,29 @@ def __unicode__(self): return self.name class Meta: + abstract = True ordering = ['name'] -class Provision(models.Model): + +class Conflict(RelatedToBase): + pkg = models.ForeignKey('main.Package', related_name='conflicts') + comparison = models.CharField(max_length=255, default='') + + +class Provision(RelatedToBase): pkg = models.ForeignKey('main.Package', related_name='provides') - name = models.CharField(max_length=255, db_index=True) # comparison must be '=' for provides - comparison = '=' - version = models.CharField(max_length=255, default='') - def __unicode__(self): - if self.version: - return u'%s=%s' % (self.name, self.version) - return self.name + @property + def comparison(self): + if self.version is not None and self.version != '': + return '=' + return None - class Meta: - ordering = ['name'] -class Replacement(models.Model): +class Replacement(RelatedToBase): pkg = models.ForeignKey('main.Package', related_name='replaces') - name = models.CharField(max_length=255, db_index=True) comparison = models.CharField(max_length=255, default='') - version = models.CharField(max_length=255, default='') - - def __unicode__(self): - if self.version: - return u'%s%s%s' % (self.name, self.comparison, self.version) - return self.name - - class Meta: - ordering = ['name'] # hook up some signals -- cgit v1.2.3-54-g00ecf From f6c2bc6c33bf1fe4fe4cfff4c0177fd296c3b587 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 May 2012 19:55:48 -0500 Subject: Simplify get_best_satisfier and get_providers We always passed values in that came off the containing package object; we can access these directly in the methods themselves. Signed-off-by: Dan McGee --- main/models.py | 51 ++++++++++++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/main/models.py b/main/models.py index 398cb88b..4b445dd0 100644 --- a/main/models.py +++ b/main/models.py @@ -231,16 +231,12 @@ def get_depends(self): """ deps = [] arches = None - if not self.arch.agnostic: - arches = self.applicable_arches() # TODO: we can use list comprehension and an 'in' query to make this more effective for dep in self.depends.order_by('optional', 'depname'): - pkg = dep.get_best_satisfier(arches, testing=self.repo.testing, - staging=self.repo.staging) + pkg = dep.get_best_satisfier() providers = None if not pkg: - providers = dep.get_providers(arches, - testing=self.repo.testing, staging=self.repo.staging) + providers = dep.get_providers() deps.append({'dep': dep, 'pkg': pkg, 'providers': providers}) return deps @@ -343,13 +339,14 @@ class PackageDepend(models.Model): optional = models.BooleanField(default=False) description = models.TextField(null=True, blank=True) - def get_best_satisfier(self, arches=None, testing=None, staging=None): + def get_best_satisfier(self): '''Find a satisfier for this dependency that best matches the given criteria. It will not search provisions, but will find packages named and matching repo characteristics if possible.''' pkgs = Package.objects.normal().filter(pkgname=self.depname) - if arches is not None: + if not self.pkg.arch.agnostic: # make sure we match architectures if possible + arches = self.pkg.applicable_arches() pkgs = pkgs.filter(arch__in=arches) if len(pkgs) == 0: # couldn't find a package in the DB @@ -362,42 +359,42 @@ def get_best_satisfier(self, arches=None, testing=None, staging=None): pkg = pkgs[0] # prevents yet more DB queries, these lists should be short; # after each grab the best available in case we remove all entries - if staging is not None: - pkgs = [p for p in pkgs if p.repo.staging == staging] + pkgs = [p for p in pkgs if p.repo.staging == self.pkg.repo.staging] if len(pkgs) > 0: pkg = pkgs[0] - if testing is not None: - pkgs = [p for p in pkgs if p.repo.testing == testing] + pkgs = [p for p in pkgs if p.repo.testing == self.pkg.repo.testing] if len(pkgs) > 0: pkg = pkgs[0] return pkg - def get_providers(self, arches=None, testing=None, staging=None): + def get_providers(self): '''Return providers of this dep. Does *not* include exact matches as it checks the Provision names only, use get_best_satisfier() instead.''' pkgs = Package.objects.normal().filter( provides__name=self.depname).order_by().distinct() - if arches is not None: + if not self.pkg.arch.agnostic: + # make sure we match architectures if possible + arches = self.pkg.applicable_arches() pkgs = pkgs.filter(arch__in=arches) # Logic here is to filter out packages that are in multiple repos if # they are not requested. For example, if testing is False, only show a # testing package if it doesn't exist in a non-testing repo. - if staging is not None: - filtered = {} - for p in pkgs: - if p.pkgname not in filtered or p.repo.staging == staging: - filtered[p.pkgname] = p - pkgs = filtered.values() - - if testing is not None: - filtered = {} - for p in pkgs: - if p.pkgname not in filtered or p.repo.testing == testing: - filtered[p.pkgname] = p - pkgs = filtered.values() + filtered = {} + for package in pkgs: + if package.pkgname not in filtered or \ + package.repo.staging == self.pkg.repo.staging: + filtered[package.pkgname] = package + pkgs = filtered.values() + + filtered = {} + for package in pkgs: + if package.pkgname not in filtered or \ + package.repo.testing == self.pkg.repo.testing: + filtered[package.pkgname] = package + pkgs = filtered.values() return pkgs -- cgit v1.2.3-54-g00ecf From 158be107e4ad682de0c9360658dfa5a72c21ee58 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 May 2012 19:57:12 -0500 Subject: Add a get_best_satisfier method to RelatedToBase This is basically what we do in PackageDepend already. Signed-off-by: Dan McGee --- packages/models.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/models.py b/packages/models.py index f57c9f3c..a4d2a556 100644 --- a/packages/models.py +++ b/packages/models.py @@ -4,7 +4,7 @@ from django.db.models.signals import pre_save from django.contrib.auth.models import User -from main.models import Arch, Repo +from main.models import Arch, Repo, Package from main.utils import set_created_field class PackageRelation(models.Model): @@ -224,6 +224,40 @@ class RelatedToBase(models.Model): name = models.CharField(max_length=255, db_index=True) version = models.CharField(max_length=255, default='') + def get_best_satisfier(self): + '''Find a satisfier for this related package that best matches the + given criteria. It will not search provisions, but will find packages + named and matching repo characteristics if possible.''' + # NOTE: this is cribbed directly from the PackageDepend method of the + # same name. Really, all of these things could use the same method if + # the PackageDepend class was moved here and field names were changed + # to match the layout we use here. + pkgs = Package.objects.normal().filter(pkgname=self.name) + if not self.pkg.arch.agnostic: + # make sure we match architectures if possible + arches = self.pkg.applicable_arches() + pkgs = pkgs.filter(arch__in=arches) + if len(pkgs) == 0: + # couldn't find a package in the DB + # it should be a virtual depend (or a removed package) + return None + if len(pkgs) == 1: + return pkgs[0] + # more than one package, see if we can't shrink it down + # grab the first though in case we fail + pkg = pkgs[0] + # prevents yet more DB queries, these lists should be short; + # after each grab the best available in case we remove all entries + pkgs = [p for p in pkgs if p.repo.staging == self.pkg.repo.staging] + if len(pkgs) > 0: + pkg = pkgs[0] + + pkgs = [p for p in pkgs if p.repo.testing == self.pkg.repo.testing] + if len(pkgs) > 0: + pkg = pkgs[0] + + return pkg + def __unicode__(self): if self.version: return u'%s%s%s' % (self.name, self.comparison, self.version) -- cgit v1.2.3-54-g00ecf From 1625cd31c334b017688a8c30631ddad60fafd8f4 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 May 2012 19:57:40 -0500 Subject: Link to provides/conflicts/replacements if we can in details template Use the newly implemented get_best_satisfier() method that is in the abstract base class for all of these types. Signed-off-by: Dan McGee --- templates/packages/details.html | 12 ++++++------ templates/packages/details_relatedto.html | 10 ++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 templates/packages/details_relatedto.html diff --git a/templates/packages/details.html b/templates/packages/details.html index 4cb6032e..358ab525 100644 --- a/templates/packages/details.html +++ b/templates/packages/details.html @@ -120,22 +120,22 @@

      Versions Elsewhere

      {% endif %}{% endwith %} - {% with pkg.provides.all as provides %}{% if provides %} + {% with pkg.provides.all as all_related %}{% if all_related %}
    - + {% endif %}{% endwith %} - {% with pkg.conflicts.all as conflicts %}{% if conflicts %} + {% with pkg.conflicts.all as all_related %}{% if all_related %} - + {% endif %}{% endwith %} - {% with pkg.replaces.all as replaces %}{% if replaces %} + {% with pkg.replaces.all as all_related %}{% if all_related %} - + {% endif %}{% endwith %} diff --git a/templates/packages/details_relatedto.html b/templates/packages/details_relatedto.html new file mode 100644 index 00000000..1ffe2884 --- /dev/null +++ b/templates/packages/details_relatedto.html @@ -0,0 +1,10 @@ +{% load package_extras %} +{% for related in all_related %} +{% with related.get_best_satisfier as best_satisfier %} +{% ifequal best_satisfier None %} +{{ related.name }}{{ related.comparison|default:"" }}{{ related.version|default:"" }}{% if not forloop.last %}, {% endif %} +{% else %} +{% pkg_details_link best_satisfier %}{{ related.comparison|default:"" }}{{related.version|default:"" }}{% if not forloop.last %}, {% endif %} +{% endifequal %} +{% endwith %} +{% endfor %} -- cgit v1.2.3-54-g00ecf From cf67e7952396121d3f7190195d812ea3f5fc7dcf Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 May 2012 20:26:16 -0500 Subject: Issue redirects from non-agnostic to agnostic URLs if unambiguous For something like "/extra/i686/apache-ant/", we can redirect to "/extra/any/apache-ant/" without ambiguity. Previously this redirected to the split packages listing with a single package, which was neither correct nor really expected. Signed-off-by: Dan McGee --- packages/views/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/views/__init__.py b/packages/views/__init__.py index c1a035d0..3e574c26 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -136,6 +136,16 @@ def details(request, name='', repo='', arch=''): return direct_to_template(request, 'packages/details.html', {'pkg': pkg, }) except Package.DoesNotExist: + arch_obj = get_object_or_404(Arch, name=arch) + # for arch='any' packages, we can issue a redirect to them if we + # have a single non-ambiguous option by changing the arch to match + # any arch-agnostic package + if not arch_obj.agnostic: + pkgs = Package.objects.select_related( + 'arch', 'repo', 'packager').filter(pkgname=name, + repo__name__iexact=repo, arch__agnostic=True) + if len(pkgs) == 1: + return redirect(pkgs[0], permanent=True) return split_package_details(request, name, repo, arch) else: pkg_data = [ -- cgit v1.2.3-54-g00ecf From 06f1bb99617e532f6b39c135370de79be7c270fa Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 May 2012 20:42:05 -0500 Subject: mirrors: add an alternate_email column We have a lot of these in the freeform text area in the mirror notes; attempt to make this data usable as necessary if we want to do some sort of mirror notification automation in the future. Signed-off-by: Dan McGee --- mirrors/admin.py | 4 +- .../0018_auto__add_field_mirror_alternate_email.py | 68 ++++++++++++++++++++++ mirrors/models.py | 1 + 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 mirrors/migrations/0018_auto__add_field_mirror_alternate_email.py diff --git a/mirrors/admin.py b/mirrors/admin.py index b7b9894c..65fff368 100644 --- a/mirrors/admin.py +++ b/mirrors/admin.py @@ -63,9 +63,9 @@ class Meta: class MirrorAdmin(admin.ModelAdmin): form = MirrorAdminForm list_display = ('name', 'tier', 'country', 'active', 'public', - 'isos', 'admin_email') + 'isos', 'admin_email', 'alternate_email') list_filter = ('tier', 'active', 'public', 'country') - search_fields = ('name',) + search_fields = ('name', 'admin_email', 'alternate_email') inlines = [ MirrorUrlInlineAdmin, MirrorRsyncInlineAdmin, diff --git a/mirrors/migrations/0018_auto__add_field_mirror_alternate_email.py b/mirrors/migrations/0018_auto__add_field_mirror_alternate_email.py new file mode 100644 index 00000000..a08699e8 --- /dev/null +++ b/mirrors/migrations/0018_auto__add_field_mirror_alternate_email.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +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('mirrors_mirror', 'alternate_email', + self.gf('django.db.models.fields.EmailField')(default='', max_length=255, blank=True), + keep_default=False) + + def backwards(self, orm): + db.delete_column('mirrors_mirror', 'alternate_email') + + 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'}), + 'alternate_email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'blank': 'True'}), + 'country': ('django_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', 'blank': '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', 'on_delete': 'models.SET_NULL'}) + }, + '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.TextField', [], {'default': "''", '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'}, + 'default': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + '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_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', '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'", 'on_delete': 'models.PROTECT', '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 8c2bd7fc..9a545b51 100644 --- a/mirrors/models.py +++ b/mirrors/models.py @@ -20,6 +20,7 @@ class Mirror(models.Model): upstream = models.ForeignKey('self', null=True, on_delete=models.SET_NULL) country = CountryField(blank=True, db_index=True) admin_email = models.EmailField(max_length=255, blank=True) + alternate_email = models.EmailField(max_length=255, blank=True) public = models.BooleanField(default=True) active = models.BooleanField(default=True) isos = models.BooleanField("ISOs", default=True) -- cgit v1.2.3-54-g00ecf From ef561bcd705e1047b1a81364ac143ce52c60defa Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 May 2012 20:54:39 -0500 Subject: Add new depends model Signed-off-by: Dan McGee --- packages/migrations/0015_auto__add_depend.py | 199 +++++++++++++++++++++++++++ packages/models.py | 7 + 2 files changed, 206 insertions(+) create mode 100644 packages/migrations/0015_auto__add_depend.py diff --git a/packages/migrations/0015_auto__add_depend.py b/packages/migrations/0015_auto__add_depend.py new file mode 100644 index 00000000..c9685ecb --- /dev/null +++ b/packages/migrations/0015_auto__add_depend.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + def forwards(self, orm): + db.create_table('packages_depend', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('version', self.gf('django.db.models.fields.CharField')(default='', max_length=255)), + ('pkg', self.gf('django.db.models.fields.related.ForeignKey')(related_name='depends_new', to=orm['main.Package'])), + ('comparison', self.gf('django.db.models.fields.CharField')(default='', max_length=255)), + ('optional', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('packages', ['Depend']) + + def backwards(self, orm): + db.delete_table('packages_depend') + + 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',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + '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': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'", 'on_delete': 'models.PROTECT', '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': '2'}), + '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.depend': { + 'Meta': {'ordering': "['name']", 'object_name': 'Depend'}, + 'comparison': ('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'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'depends_new'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.flagrequest': { + 'Meta': {'object_name': 'FlagRequest'}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'is_legitimate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'num_packages': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + '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', 'db_index': 'True'}), + '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'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index a4d2a556..c7b1cab4 100644 --- a/packages/models.py +++ b/packages/models.py @@ -268,6 +268,13 @@ class Meta: ordering = ['name'] +class Depend(RelatedToBase): + pkg = models.ForeignKey('main.Package', related_name='depends_new') + comparison = models.CharField(max_length=255, default='') + optional = models.BooleanField(default=False) + description = models.TextField(null=True, blank=True) + + class Conflict(RelatedToBase): pkg = models.ForeignKey('main.Package', related_name='conflicts') comparison = models.CharField(max_length=255, default='') -- cgit v1.2.3-54-g00ecf From cc44fdbea59596daf106e48acdb3f4137988d0d9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 May 2012 21:10:14 -0500 Subject: Migrate package depends data into new model Signed-off-by: Dan McGee --- packages/migrations/0016_copy_depends_data.py | 246 ++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 packages/migrations/0016_copy_depends_data.py diff --git a/packages/migrations/0016_copy_depends_data.py b/packages/migrations/0016_copy_depends_data.py new file mode 100644 index 00000000..a4b55d4e --- /dev/null +++ b/packages/migrations/0016_copy_depends_data.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +import re +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + depends_on = ( + ('main', '0038_add_depends_optional_description.py'), + ) + + def forwards(self, orm): + Depend = orm['packages.Depend'] + vcmp_re = re.compile(r"^(>=|<=|=|>|<)(.*)$") + for old in orm['main.PackageDepend'].objects.all(): + comp = ver = '' + m = vcmp_re.match(old.depvcmp) + if m: + comp = m.group(1) + ver = m.group(2) + new_dep = Depend(pkg_id=old.pkg_id, name=old.depname, + comparison=comp, version=ver, optional=old.optional, + description=old.description) + new_dep.save(force_insert=True) + + def backwards(self, orm): + orm['packages.Depend'].objects.all().delete() + + 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'"}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + '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',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + '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': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'", 'on_delete': 'models.PROTECT', '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', [], {'related_name': "'depends'", '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': '2'}), + '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.todolist': { + 'Meta': {'object_name': 'Todolist', 'db_table': "'todolists'"}, + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'on_delete': 'models.PROTECT'}), + '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']"}) + }, + '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.depend': { + 'Meta': {'ordering': "['name']", 'object_name': 'Depend'}, + 'comparison': ('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'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'depends_new'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.flagrequest': { + 'Meta': {'object_name': 'FlagRequest'}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'is_legitimate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'num_packages': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + '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', 'db_index': 'True'}), + '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'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + } + } + + complete_apps = ['main', 'packages'] + symmetrical = True -- cgit v1.2.3-54-g00ecf From 72a92102df4999dbcc370064707c9026d51c4fe7 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 May 2012 21:29:03 -0500 Subject: Switch to usage of new Depend object Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 18 +++++++++------- devel/views.py | 6 +++--- main/models.py | 7 +++--- packages/models.py | 34 ++++++++++++++++++++++++++---- packages/utils.py | 6 +++--- templates/packages/details_depend.html | 6 +++--- templates/packages/details_requiredby.html | 2 +- 7 files changed, 54 insertions(+), 25 deletions(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index fd8e3979..47294d9a 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -29,9 +29,9 @@ from django.db.utils import IntegrityError from devel.utils import UserFinder -from main.models import Arch, Package, PackageDepend, PackageFile, Repo +from main.models import Arch, Package, PackageFile, Repo from main.utils import utc_now -from packages.models import Conflict, Provision, Replacement +from packages.models import Depend, Conflict, Provision, Replacement logging.basicConfig( @@ -141,19 +141,21 @@ def full_version(self): return u'%s-%s' % (self.ver, self.rel) -DEPEND_RE = re.compile(r"^(.+?)((>=|<=|=|>|<)(.*))?$") +DEPEND_RE = re.compile(r"^(.+?)((>=|<=|=|>|<)(.+))?$") def create_depend(package, dep_str, optional=False): - depend = PackageDepend(pkg=package, optional=optional) + depend = Depend(pkg=package, optional=optional) # lop off any description first parts = dep_str.split(':', 1) if len(parts) > 1: depend.description = parts[1].strip() match = DEPEND_RE.match(parts[0].strip()) if match: - depend.depname = match.group(1) - if match.group(2): - depend.depvcmp = match.group(2) + depend.name = match.group(1) + if match.group(3): + depend.comparison = match.group(3) + if match.group(4): + related.version = match.group(4) else: logger.warning('Package %s had unparsable depend string %s', package.pkgname, dep_str) @@ -232,7 +234,7 @@ def populate_pkg(dbpkg, repopkg, force=False, timestamp=None): dbpkg.depends.all().delete() deps = [create_depend(dbpkg, y) for y in repopkg.depends] deps += [create_depend(dbpkg, y, True) for y in repopkg.optdepends] - PackageDepend.objects.bulk_create(deps) + Depend.objects.bulk_create(deps) dbpkg.conflicts.all().delete() conflicts = [create_related(Conflict, dbpkg, y) for y in repopkg.conflicts] diff --git a/devel/views.py b/devel/views.py index 0f1c8d15..16b6acc6 100644 --- a/devel/views.py +++ b/devel/views.py @@ -26,11 +26,11 @@ from django.utils.http import http_date from .models import UserProfile -from main.models import Package, PackageDepend, PackageFile, TodolistPkg +from main.models import Package, PackageFile, TodolistPkg from main.models import Arch, Repo from main.utils import utc_now from news.models import News -from packages.models import PackageRelation, Signoff +from packages.models import PackageRelation, Signoff, Depend from packages.utils import get_signoff_groups from todolists.utils import get_annotated_todolists from .utils import get_annotated_maintainers, UserFinder @@ -267,7 +267,7 @@ def report(request, report_name, username=None): elif report_name == 'unneeded-orphans': title = 'Orphan packages required by no other packages' owned = PackageRelation.objects.all().values('pkgbase') - required = PackageDepend.objects.all().values('depname') + required = Depend.objects.all().values('name') # The two separate calls to exclude is required to do the right thing packages = packages.exclude(pkgbase__in=owned).exclude( pkgname__in=required) diff --git a/main/models.py b/main/models.py index 4b445dd0..f17d4a4d 100644 --- a/main/models.py +++ b/main/models.py @@ -180,11 +180,12 @@ def get_requiredby(self): list slim by including the corresponding package in the same testing category as this package if that check makes sense. """ + from packages.models import Depend provides = set(self.provides.values_list('name', flat=True)) provides.add(self.pkgname) - requiredby = PackageDepend.objects.select_related('pkg', + requiredby = Depend.objects.select_related('pkg', 'pkg__arch', 'pkg__repo').filter( - depname__in=provides).order_by( + name__in=provides).order_by( 'pkg__pkgname', 'pkg__arch__name', 'pkg__repo__name') if not self.arch.agnostic: # make sure we match architectures if possible @@ -232,7 +233,7 @@ def get_depends(self): deps = [] arches = None # TODO: we can use list comprehension and an 'in' query to make this more effective - for dep in self.depends.order_by('optional', 'depname'): + for dep in self.depends.order_by('optional', 'name'): pkg = dep.get_best_satisfier() providers = None if not pkg: diff --git a/packages/models.py b/packages/models.py index c7b1cab4..cb65f1f1 100644 --- a/packages/models.py +++ b/packages/models.py @@ -228,10 +228,6 @@ def get_best_satisfier(self): '''Find a satisfier for this related package that best matches the given criteria. It will not search provisions, but will find packages named and matching repo characteristics if possible.''' - # NOTE: this is cribbed directly from the PackageDepend method of the - # same name. Really, all of these things could use the same method if - # the PackageDepend class was moved here and field names were changed - # to match the layout we use here. pkgs = Package.objects.normal().filter(pkgname=self.name) if not self.pkg.arch.agnostic: # make sure we match architectures if possible @@ -258,6 +254,36 @@ def get_best_satisfier(self): return pkg + def get_providers(self): + '''Return providers of this related package. Does *not* include exact + matches as it checks the Provision names only, use get_best_satisfier() + instead for exact matches.''' + pkgs = Package.objects.normal().filter( + provides__name=self.name).order_by().distinct() + if not self.pkg.arch.agnostic: + # make sure we match architectures if possible + arches = self.pkg.applicable_arches() + pkgs = pkgs.filter(arch__in=arches) + + # Logic here is to filter out packages that are in multiple repos if + # they are not requested. For example, if testing is False, only show a + # testing package if it doesn't exist in a non-testing repo. + filtered = {} + for package in pkgs: + if package.pkgname not in filtered or \ + package.repo.staging == self.pkg.repo.staging: + filtered[package.pkgname] = package + pkgs = filtered.values() + + filtered = {} + for package in pkgs: + if package.pkgname not in filtered or \ + package.repo.testing == self.pkg.repo.testing: + filtered[package.pkgname] = package + pkgs = filtered.values() + + return pkgs + def __unicode__(self): if self.version: return u'%s%s%s' % (self.name, self.comparison, self.version) diff --git a/packages/utils.py b/packages/utils.py index 8d00bd68..82313472 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -7,10 +7,10 @@ from django.db.models import Count, Max, F from django.contrib.auth.models import User -from main.models import Package, PackageDepend, PackageFile, Arch, Repo +from main.models import Package, PackageFile, Arch, Repo from main.utils import cache_function, groupby_preserve_order, PackageStandin from .models import (PackageGroup, PackageRelation, - License, Conflict, Provision, Replacement, + License, Depend, Conflict, Provision, Replacement, SignoffSpecification, Signoff, DEFAULT_SIGNOFF_SPEC) @cache_function(127) @@ -451,7 +451,7 @@ def default(self, obj): return obj.name.lower() if isinstance(obj, (PackageGroup, License)): return obj.name - if isinstance(obj, (Conflict, Provision, Replacement, PackageDepend)): + if isinstance(obj, (Depend, Conflict, Provision, Replacement)): return unicode(obj) elif isinstance(obj, User): return obj.username diff --git a/templates/packages/details_depend.html b/templates/packages/details_depend.html index 8b6e85c9..0cf2c36a 100644 --- a/templates/packages/details_depend.html +++ b/templates/packages/details_depend.html @@ -2,12 +2,12 @@
  • {% ifequal depend.pkg None %} {% if depend.providers %} -{{ depend.dep.depname }} ({% multi_pkg_details depend.providers %}) +{{ depend.dep.name }}{{ depend.dep.comparison|default:"" }}{{ depend.dep.version|default:"" }} ({% multi_pkg_details depend.providers %}) {% else %} -{{ depend.dep.depname }} (virtual) +{{ depend.dep.name }}{{ depend.dep.comparison|default:"" }}{{ depend.dep.version|default:"" }} (virtual) {% endif %} {% else %} -{% pkg_details_link depend.pkg %}{{ depend.dep.depvcmp|default:"" }} +{% pkg_details_link depend.pkg %}{{ depend.dep.comparison|default:"" }}{{ depend.dep.version|default:"" }} {% if depend.pkg.repo.testing %} (testing){% endif %} {% if depend.pkg.repo.staging %} (staging){% endif %} {% endifequal %} diff --git a/templates/packages/details_requiredby.html b/templates/packages/details_requiredby.html index c7697289..ecc92b29 100644 --- a/templates/packages/details_requiredby.html +++ b/templates/packages/details_requiredby.html @@ -1,6 +1,6 @@ {% load package_extras %}
  • {% pkg_details_link req.pkg %} -{% if req.depname != pkg.pkgname %}(requires {{ req.depname }}){% endif %} +{% if req.name != pkg.pkgname %}(requires {{ req.name }}){% endif %} {% if req.pkg.repo.testing %}(testing){% endif %} {% if req.pkg.repo.staging %}(staging){% endif %} {% if req.optional %}(optional){% endif %} -- cgit v1.2.3-54-g00ecf From e1f9a3c44a1449a36f3981b868814f3d1f65d70d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 May 2012 21:35:28 -0500 Subject: Drop old PackageDepend model Signed-off-by: Dan McGee --- main/migrations/0061_auto__del_packagedepend.py | 135 ++++++++++++++++++++++++ main/models.py | 71 ------------- packages/models.py | 2 +- 3 files changed, 136 insertions(+), 72 deletions(-) create mode 100644 main/migrations/0061_auto__del_packagedepend.py diff --git a/main/migrations/0061_auto__del_packagedepend.py b/main/migrations/0061_auto__del_packagedepend.py new file mode 100644 index 00000000..6cb1f68f --- /dev/null +++ b/main/migrations/0061_auto__del_packagedepend.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + depends_on = ( + ('packages', '0016_copy_depends_data.py'), + ) + + def forwards(self, orm): + db.delete_table('package_depends') + + def backwards(self, orm): + db.create_table('package_depends', ( + ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('depvcmp', self.gf('django.db.models.fields.CharField')(default='', max_length=255)), + ('pkg', self.gf('django.db.models.fields.related.ForeignKey')(related_name='depends', to=orm['main.Package'])), + ('depname', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('optional', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + )) + db.send_create_signal('main', ['PackageDepend']) + + 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'"}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + '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',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + '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': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Repo']"}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) + }, + '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': '2'}), + '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.todolist': { + 'Meta': {'object_name': 'Todolist', 'db_table': "'todolists'"}, + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'on_delete': 'models.PROTECT'}), + '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']"}) + } + } + + complete_apps = ['main'] diff --git a/main/models.py b/main/models.py index f17d4a4d..04d8da8f 100644 --- a/main/models.py +++ b/main/models.py @@ -333,77 +333,6 @@ def __unicode__(self): class Meta: db_table = 'package_files' -class PackageDepend(models.Model): - pkg = models.ForeignKey(Package, related_name='depends') - depname = models.CharField(max_length=255, db_index=True) - depvcmp = models.CharField(max_length=255, default='') - optional = models.BooleanField(default=False) - description = models.TextField(null=True, blank=True) - - def get_best_satisfier(self): - '''Find a satisfier for this dependency that best matches the given - criteria. It will not search provisions, but will find packages named - and matching repo characteristics if possible.''' - pkgs = Package.objects.normal().filter(pkgname=self.depname) - if not self.pkg.arch.agnostic: - # make sure we match architectures if possible - arches = self.pkg.applicable_arches() - pkgs = pkgs.filter(arch__in=arches) - if len(pkgs) == 0: - # couldn't find a package in the DB - # it should be a virtual depend (or a removed package) - return None - if len(pkgs) == 1: - return pkgs[0] - # more than one package, see if we can't shrink it down - # grab the first though in case we fail - pkg = pkgs[0] - # prevents yet more DB queries, these lists should be short; - # after each grab the best available in case we remove all entries - pkgs = [p for p in pkgs if p.repo.staging == self.pkg.repo.staging] - if len(pkgs) > 0: - pkg = pkgs[0] - - pkgs = [p for p in pkgs if p.repo.testing == self.pkg.repo.testing] - if len(pkgs) > 0: - pkg = pkgs[0] - - return pkg - - def get_providers(self): - '''Return providers of this dep. Does *not* include exact matches as it - checks the Provision names only, use get_best_satisfier() instead.''' - pkgs = Package.objects.normal().filter( - provides__name=self.depname).order_by().distinct() - if not self.pkg.arch.agnostic: - # make sure we match architectures if possible - arches = self.pkg.applicable_arches() - pkgs = pkgs.filter(arch__in=arches) - - # Logic here is to filter out packages that are in multiple repos if - # they are not requested. For example, if testing is False, only show a - # testing package if it doesn't exist in a non-testing repo. - filtered = {} - for package in pkgs: - if package.pkgname not in filtered or \ - package.repo.staging == self.pkg.repo.staging: - filtered[package.pkgname] = package - pkgs = filtered.values() - - filtered = {} - for package in pkgs: - if package.pkgname not in filtered or \ - package.repo.testing == self.pkg.repo.testing: - filtered[package.pkgname] = package - pkgs = filtered.values() - - return pkgs - - def __unicode__(self): - return "%s%s" % (self.depname, self.depvcmp) - - class Meta: - db_table = 'package_depends' class Todolist(models.Model): creator = models.ForeignKey(User, on_delete=models.PROTECT) diff --git a/packages/models.py b/packages/models.py index cb65f1f1..c3a16fc5 100644 --- a/packages/models.py +++ b/packages/models.py @@ -295,7 +295,7 @@ class Meta: class Depend(RelatedToBase): - pkg = models.ForeignKey('main.Package', related_name='depends_new') + pkg = models.ForeignKey('main.Package', related_name='depends') comparison = models.CharField(max_length=255, default='') optional = models.BooleanField(default=False) description = models.TextField(null=True, blank=True) -- cgit v1.2.3-54-g00ecf From 3f9aeb45c2a2b498a389bacc92b5f56d9feb4329 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 May 2012 09:54:15 -0500 Subject: reporead: fix copy/paste issue Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index 47294d9a..2e8c4625 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -155,7 +155,7 @@ def create_depend(package, dep_str, optional=False): if match.group(3): depend.comparison = match.group(3) if match.group(4): - related.version = match.group(4) + depend.version = match.group(4) else: logger.warning('Package %s had unparsable depend string %s', package.pkgname, dep_str) -- cgit v1.2.3-54-g00ecf From eef1ee7051093b9f6e74ab5669af8c57983872d9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 May 2012 16:32:17 -0500 Subject: Add RSS links to /news/ and /packages/ URLs These were available only from the home page, but it makes sense to advertise them on the corresponding index pages too. Signed-off-by: Dan McGee --- templates/news/list.html | 4 ++++ templates/packages/search.html | 1 + 2 files changed, 5 insertions(+) diff --git a/templates/news/list.html b/templates/news/list.html index a72a2dda..e85ceced 100644 --- a/templates/news/list.html +++ b/templates/news/list.html @@ -1,6 +1,10 @@ {% extends "base.html" %} {% block title %}Arch Linux - News{% endblock %} +{% block head %} + +{% endblock %} + {% block content %}
    diff --git a/templates/packages/search.html b/templates/packages/search.html index ebd4e6c4..df5c5791 100644 --- a/templates/packages/search.html +++ b/templates/packages/search.html @@ -8,6 +8,7 @@ {% block head %} {% if is_paginated and page_obj.number > 1 %}{% endif %} + {% endblock %} {% block content %} -- cgit v1.2.3-54-g00ecf From d685d51e8c80f29d4f14977a0d8088534e6626c4 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 20 May 2012 13:21:59 -0500 Subject: Ensure we use last_modified date from News in headers We were actually using the postdate attribute rather than last_modified, which means any News objects that get edited would not trigger an update of the feed. Signed-off-by: Dan McGee --- feeds.py | 4 ++-- main/utils.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/feeds.py b/feeds.py index ee856f62..2e9b349b 100644 --- a/feeds.py +++ b/feeds.py @@ -106,13 +106,13 @@ def item_categories(self, item): def news_etag(request, *args, **kwargs): - latest = retrieve_latest(News) + latest = retrieve_latest(News, 'last_modified') if latest: return hashlib.md5(str(latest)).hexdigest() return None def news_last_modified(request, *args, **kwargs): - return retrieve_latest(News) + return retrieve_latest(News, 'last_modified') class NewsFeed(Feed): feed_type = GuidNotPermalinkFeed diff --git a/main/utils.py b/main/utils.py index e7e47c53..b7cb19f4 100644 --- a/main/utils.py +++ b/main/utils.py @@ -72,7 +72,7 @@ def refresh_latest(**kwargs): cache.set(cache_key, None, INVALIDATE_TIMEOUT) -def retrieve_latest(sender): +def retrieve_latest(sender, latest_by=None): # 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__ @@ -80,8 +80,9 @@ def retrieve_latest(sender): if latest: return latest try: - latest_by = sender._meta.get_latest_by - latest = sender.objects.values(latest_by).latest()[latest_by] + if latest_by is None: + latest_by = sender._meta.get_latest_by + latest = sender.objects.values(latest_by).latest(latest_by)[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. -- cgit v1.2.3-54-g00ecf From a5fafae94f23ca2cf138cf07774e2c2a1a7d993a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 7 Jun 2012 20:29:07 -0500 Subject: Fix a few minor markup errors Signed-off-by: Dan McGee --- templates/public/donate.html | 2 +- templates/releng/result_section.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/public/donate.html b/templates/public/donate.html index 0809e2a6..b1d52f88 100644 --- a/templates/public/donate.html +++ b/templates/public/donate.html @@ -23,7 +23,7 @@

    Donate to Arch Linux

    Monetary donations

    -

    Financial contributions are accepted via Click&Pledge. +

    Financial contributions are accepted via Click&Pledge. Arch Linux is a member project of the Software in the Public Interest, Inc. non-profit corporation. Funds are used for hosting costs, server hardware diff --git a/templates/releng/result_section.html b/templates/releng/result_section.html index 91a75613..ae951ebe 100644 --- a/templates/releng/result_section.html +++ b/templates/releng/result_section.html @@ -1,6 +1,6 @@ {% load url from future %}

  • - -- cgit v1.2.3-54-g00ecf From 1b03069f0d9e0397a7ff07404343c9400bbcfa1c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 7 Jun 2012 20:53:52 -0500 Subject: Use 3 decimal places for showing compression ratio Otherwise there are too many grouped under each value. Signed-off-by: Dan McGee --- devel/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devel/views.py b/devel/views.py index 16b6acc6..78ed26f2 100644 --- a/devel/views.py +++ b/devel/views.py @@ -234,7 +234,7 @@ def report(request, report_name, username=None): package.installed_size_pretty = filesizeformat( package.installed_size) ratio = package.compressed_size / float(package.installed_size) - package.ratio = '%.2f' % ratio + package.ratio = '%.3f' % ratio package.compress_type = package.filename.split('.')[-1] elif report_name == 'uncompressed-man': title = 'Packages with uncompressed manpages' -- cgit v1.2.3-54-g00ecf From bbcbde0197d4862b5acc595b17bc5051780dbc9e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 20 Jun 2012 17:03:26 -0500 Subject: Add a last_modified field to user profiles A behind the scenes field that might be slightly useful. Signed-off-by: Dan McGee --- ...08_auto__add_field_userprofile_last_modified.py | 108 +++++++++++++++++++++ devel/models.py | 18 +++- 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 devel/migrations/0008_auto__add_field_userprofile_last_modified.py diff --git a/devel/migrations/0008_auto__add_field_userprofile_last_modified.py b/devel/migrations/0008_auto__add_field_userprofile_last_modified.py new file mode 100644 index 00000000..2695987a --- /dev/null +++ b/devel/migrations/0008_auto__add_field_userprofile_last_modified.py @@ -0,0 +1,108 @@ +# -*- coding: 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('user_profiles', 'last_modified', + self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2000, 1, 1, 0, 0)), + keep_default=False) + + def backwards(self, orm): + db.delete_column('user_profiles', 'last_modified') + + 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'}) + }, + 'devel.masterkey': { + 'Meta': {'ordering': "('created',)", 'object_name': 'MasterKey'}, + 'created': ('django.db.models.fields.DateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'masterkey_owner'", 'to': "orm['auth.User']"}), + 'pgp_key': ('devel.fields.PGPKeyField', [], {'max_length': '40'}), + 'revoked': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'revoker': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'masterkey_revoker'", 'to': "orm['auth.User']"}) + }, + 'devel.pgpsignature': { + 'Meta': {'object_name': 'PGPSignature'}, + 'created': ('django.db.models.fields.DateField', [], {}), + 'expires': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'signee': ('devel.fields.PGPKeyField', [], {'max_length': '40'}), + 'signer': ('devel.fields.PGPKeyField', [], {'max_length': '40'}), + 'valid': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'devel.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'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', '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'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {}), + 'latin_name': ('django.db.models.fields.CharField', [], {'max_length': '255', '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'}), + 'pgp_key': ('devel.fields.PGPKeyField', [], {'max_length': '40', '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'}) + }, + 'main.repo': { + 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"}, + 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}), + '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'}) + } + } + + complete_apps = ['devel'] diff --git a/devel/models.py b/devel/models.py index fd5a0347..fd5df00a 100644 --- a/devel/models.py +++ b/devel/models.py @@ -2,11 +2,12 @@ import pytz from django.db import models +from django.db.models.signals import pre_save from django.contrib.auth.models import User from django_countries import CountryField from .fields import PGPKeyField -from main.utils import make_choice +from main.utils import make_choice, utc_now class UserProfile(models.Model): @@ -44,6 +45,7 @@ class UserProfile(models.Model): allowed_repos = models.ManyToManyField('main.Repo', blank=True) latin_name = models.CharField(max_length=255, null=True, blank=True, help_text="Latin-form name; used only for non-Latin full names") + last_modified = models.DateTimeField(editable=False) class Meta: db_table = 'user_profiles' @@ -96,4 +98,18 @@ class Meta: def __unicode__(self): return u'%s → %s' % (self.signer, self.signee) + +def set_last_modified(sender, **kwargs): + '''This will set the 'last_modified' field on the user profile to the + current UTC time when either the profile is updated. For use as a pre_save + signal handler.''' + obj = kwargs['instance'] + if hasattr(obj, 'last_modified'): + obj.last_modified = utc_now() + + +# connect signals needed to keep cache in line with reality +pre_save.connect(set_last_modified, sender=UserProfile, + dispatch_uid="devel.models") + # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From b547d41dbf338fb75eb2c6ae05da143a5cd32c74 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 1 Jul 2012 19:40:42 -0500 Subject: Remove no-longer necessary delayed imports of Package Since commit 158be107e4ad6, we have been importing the Package model at the top-level in this file, so we can kill this code that was never updated. This should also give us back any performance hit we were seeing from the delayed imports. Signed-off-by: Dan McGee --- packages/models.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/models.py b/packages/models.py index c3a16fc5..2c6ad43c 100644 --- a/packages/models.py +++ b/packages/models.py @@ -26,8 +26,6 @@ class PackageRelation(models.Model): created = models.DateTimeField(editable=False) def get_associated_packages(self): - # TODO: delayed import to avoid circular reference - from main.models import Package return Package.objects.normal().filter(pkgbase=self.pkgbase) def repositories(self): @@ -146,8 +144,6 @@ class Signoff(models.Model): @property def packages(self): - # TODO: delayed import to avoid circular reference - from main.models import Package return Package.objects.normal().filter(pkgbase=self.pkgbase, pkgver=self.pkgver, pkgrel=self.pkgrel, epoch=self.epoch, arch=self.arch, repo=self.repo) @@ -202,14 +198,15 @@ class PackageGroup(models.Model): Represents a group a package is in. There is no actual group entity, only names that link to given packages. ''' - pkg = models.ForeignKey('main.Package', related_name='groups') + pkg = models.ForeignKey(Package, related_name='groups') name = models.CharField(max_length=255, db_index=True) def __unicode__(self): return "%s: %s" % (self.name, self.pkg) + class License(models.Model): - pkg = models.ForeignKey('main.Package', related_name='licenses') + pkg = models.ForeignKey(Package, related_name='licenses') name = models.CharField(max_length=255) def __unicode__(self): @@ -295,19 +292,19 @@ class Meta: class Depend(RelatedToBase): - pkg = models.ForeignKey('main.Package', related_name='depends') + pkg = models.ForeignKey(Package, related_name='depends') comparison = models.CharField(max_length=255, default='') optional = models.BooleanField(default=False) description = models.TextField(null=True, blank=True) class Conflict(RelatedToBase): - pkg = models.ForeignKey('main.Package', related_name='conflicts') + pkg = models.ForeignKey(Package, related_name='conflicts') comparison = models.CharField(max_length=255, default='') class Provision(RelatedToBase): - pkg = models.ForeignKey('main.Package', related_name='provides') + pkg = models.ForeignKey(Package, related_name='provides') # comparison must be '=' for provides @property @@ -318,7 +315,7 @@ def comparison(self): class Replacement(RelatedToBase): - pkg = models.ForeignKey('main.Package', related_name='replaces') + pkg = models.ForeignKey(Package, related_name='replaces') comparison = models.CharField(max_length=255, default='') -- cgit v1.2.3-54-g00ecf From 43b5c29b3d89cc2e7e7109bb3c7717a87cfc67b5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 1 Jul 2012 19:57:16 -0500 Subject: Add new package Update model This will be used to track updates to package as we do them during reporead. By storing enough relevant fields from the package object, we should be able to produce a useful report on a regular basis of what has been happening in the repositories. Signed-off-by: Dan McGee --- packages/admin.py | 14 +- packages/migrations/0017_auto__add_update.py | 226 +++++++++++++++++++++++++++ packages/models.py | 64 +++++++- 3 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 packages/migrations/0017_auto__add_update.py diff --git a/packages/admin.py b/packages/admin.py index 0589209f..d43cecce 100644 --- a/packages/admin.py +++ b/packages/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin -from .models import PackageRelation, FlagRequest, Signoff, SignoffSpecification +from .models import (PackageRelation, FlagRequest, + Signoff, SignoffSpecification, Update) class PackageRelationAdmin(admin.ModelAdmin): list_display = ('pkgbase', 'user', 'type', 'created') @@ -9,6 +10,7 @@ class PackageRelationAdmin(admin.ModelAdmin): ordering = ('pkgbase', 'user') date_hierarchy = 'created' + class FlagRequestAdmin(admin.ModelAdmin): list_display = ('pkgbase', 'version', 'repo', 'created', 'who', 'is_spam', 'is_legitimate', 'message') @@ -35,9 +37,19 @@ class SignoffSpecificationAdmin(admin.ModelAdmin): date_hierarchy = 'created' +class UpdateAdmin(admin.ModelAdmin): + list_display = ('pkgname', 'repo', 'arch', 'action_flag', + 'old_version', 'new_version', 'created') + list_filter = ('action_flag', 'repo', 'arch') + search_fields = ('pkgname',) + ordering = ('-created',) + date_hierarchy = 'created' + + admin.site.register(PackageRelation, PackageRelationAdmin) admin.site.register(FlagRequest, FlagRequestAdmin) admin.site.register(Signoff, SignoffAdmin) admin.site.register(SignoffSpecification, SignoffSpecificationAdmin) +admin.site.register(Update, UpdateAdmin) # vim: set ts=4 sw=4 et: diff --git a/packages/migrations/0017_auto__add_update.py b/packages/migrations/0017_auto__add_update.py new file mode 100644 index 00000000..c7c16d4e --- /dev/null +++ b/packages/migrations/0017_auto__add_update.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.create_table('packages_update', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('package', self.gf('django.db.models.fields.related.ForeignKey')(related_name='updates', null=True, on_delete=models.SET_NULL, to=orm['main.Package'])), + ('repo', self.gf('django.db.models.fields.related.ForeignKey')(related_name='updates', to=orm['main.Repo'])), + ('arch', self.gf('django.db.models.fields.related.ForeignKey')(related_name='updates', to=orm['main.Arch'])), + ('pkgname', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('pkgbase', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('action_flag', self.gf('django.db.models.fields.PositiveSmallIntegerField')()), + ('created', self.gf('django.db.models.fields.DateTimeField')()), + ('old_pkgver', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)), + ('old_pkgrel', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)), + ('old_epoch', self.gf('django.db.models.fields.PositiveIntegerField')(null=True)), + ('new_pkgver', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)), + ('new_pkgrel', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)), + ('new_epoch', self.gf('django.db.models.fields.PositiveIntegerField')(null=True)), + )) + db.send_create_signal('packages', ['Update']) + + + def backwards(self, orm): + db.delete_table('packages_update') + + + 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',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + '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': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'", 'on_delete': 'models.PROTECT', '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': '2'}), + '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.depend': { + 'Meta': {'ordering': "['name']", 'object_name': 'Depend'}, + 'comparison': ('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'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'depends'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.flagrequest': { + 'Meta': {'object_name': 'FlagRequest'}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'is_legitimate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'num_packages': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + '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', 'db_index': 'True'}), + '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'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'packages.update': { + 'Meta': {'object_name': 'Update'}, + 'action_flag': ('django.db.models.fields.PositiveSmallIntegerField', [], {}), + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Arch']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'new_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'new_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'old_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Package']"}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Repo']"}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index 2c6ad43c..5ee06575 100644 --- a/packages/models.py +++ b/packages/models.py @@ -2,11 +2,13 @@ from django.db import models from django.db.models.signals import pre_save +from django.contrib.admin.models import ADDITION, CHANGE, DELETION from django.contrib.auth.models import User from main.models import Arch, Repo, Package from main.utils import set_created_field + class PackageRelation(models.Model): ''' Represents maintainership (or interest) in a package by a given developer. @@ -193,6 +195,66 @@ def who(self): def __unicode__(self): return u'%s from %s on %s' % (self.pkgbase, self.who(), self.created) + +UPDATE_ACTION_CHOICES = ( + (ADDITION, 'Addition'), + (CHANGE, 'Change'), + (DELETION, 'Deletion'), +) + + +class Update(models.Model): + package = models.ForeignKey(Package, related_name="updates", + null=True, on_delete=models.SET_NULL) + repo = models.ForeignKey(Repo, related_name="updates") + arch = models.ForeignKey(Arch, related_name="updates") + pkgname = models.CharField(max_length=255) + pkgbase = models.CharField(max_length=255) + action_flag = models.PositiveSmallIntegerField('action flag', + choices=UPDATE_ACTION_CHOICES) + created = models.DateTimeField(editable=False) + + old_pkgver = models.CharField(max_length=255, null=True) + old_pkgrel = models.CharField(max_length=255, null=True) + old_epoch = models.PositiveIntegerField(null=True) + + new_pkgver = models.CharField(max_length=255, null=True) + new_pkgrel = models.CharField(max_length=255, null=True) + new_epoch = models.PositiveIntegerField(null=True) + + class Meta: + get_latest_by = 'created' + + def is_addition(self): + return self.action_flag == ADDITION + + def is_change(self): + return self.action_flag == CHANGE + + def is_deletion(self): + return self.action_flag == DELETION + + @property + def old_version(self): + if self.action_flag == ADDITION: + return None + if self.old_epoch > 0: + return u'%d:%s-%s' % (self.old_epoch, self.old_pkgver, self.old_pkgrel) + return u'%s-%s' % (self.old_pkgver, self.old_pkgrel) + + @property + def new_version(self): + if self.action_flag == DELETION: + return None + if self.new_epoch > 0: + return u'%d:%s-%s' % (self.new_epoch, self.new_pkgver, self.new_pkgrel) + return u'%s-%s' % (self.new_pkgver, self.new_pkgrel) + + def __unicode__(self): + return u'%s of %s on %s' % (self.get_action_flag_display(), + self.pkgname, self.created) + + class PackageGroup(models.Model): ''' Represents a group a package is in. There is no actual group entity, @@ -320,7 +382,7 @@ class Replacement(RelatedToBase): # hook up some signals -for sender in (PackageRelation, SignoffSpecification, Signoff): +for sender in (PackageRelation, SignoffSpecification, Signoff, Update): pre_save.connect(set_created_field, sender=sender, dispatch_uid="packages.models") -- cgit v1.2.3-54-g00ecf From a87fe016d1a1bf7fdcd2b19f515aa72a5b93db2b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 1 Jul 2012 20:21:34 -0500 Subject: Log package updates during reporead invocation This adds a Manager and log_update method to help log all updates made to the packages table during reporead runs. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 7 ++++++- packages/models.py | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index 2e8c4625..4e242af1 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -14,6 +14,7 @@ """ from collections import defaultdict +from copy import copy import io import os import re @@ -31,7 +32,7 @@ from devel.utils import UserFinder from main.models import Arch, Package, PackageFile, Repo from main.utils import utc_now -from packages.models import Depend, Conflict, Provision, Replacement +from packages.models import Depend, Conflict, Provision, Replacement, Update logging.basicConfig( @@ -362,6 +363,7 @@ def db_update(archname, reponame, pkgs, force=False): try: with transaction.commit_on_success(): populate_pkg(dbpkg, pkg, timestamp=utc_now()) + Update.objects.log_update(None, dbpkg) except IntegrityError: logger.warning("Could not add package %s; " "not fatal if another thread beat us to it.", @@ -372,6 +374,7 @@ def db_update(archname, reponame, pkgs, force=False): logger.info("Removing package %s", pkgname) dbpkg = dbdict[pkgname] with transaction.commit_on_success(): + Update.objects.log_update(dbpkg, None) # no race condition here as long as simultaneous threads both # issue deletes; second delete will be a no-op delete_pkg_files(dbpkg) @@ -399,7 +402,9 @@ def db_update(archname, reponame, pkgs, force=False): logger.debug("Package %s was already updated", pkg.name) continue logger.info("Updating package %s", pkg.name) + prevpkg = copy(dbpkg) populate_pkg(dbpkg, pkg, force=force, timestamp=timestamp) + Update.objects.log_update(prevpkg, dbpkg) logger.info('Finished updating arch: %s', archname) diff --git a/packages/models.py b/packages/models.py index 5ee06575..04f35f9d 100644 --- a/packages/models.py +++ b/packages/models.py @@ -203,6 +203,40 @@ def __unicode__(self): ) +class UpdateManager(models.Manager): + def log_update(self, old_pkg, new_pkg): + '''Utility method to help log an update. This will determine the type + based on how many packages are passed in, and will pull the relevant + necesary fields off the given packages.''' + update = Update() + if new_pkg: + update.action_flag = ADDITION + update.package = new_pkg + update.arch = new_pkg.arch + update.repo = new_pkg.repo + update.pkgname = new_pkg.pkgname + update.pkgbase = new_pkg.pkgbase + update.new_pkgver = new_pkg.pkgver + update.new_pkgrel = new_pkg.pkgrel + update.new_epoch = new_pkg.epoch + if old_pkg: + if new_pkg: + update.action_flag = CHANGE + else: + update.action_flag = DELETION + update.arch = old_pkg.arch + update.repo = old_pkg.repo + update.pkgname = old_pkg.pkgname + update.pkgbase = old_pkg.pkgbase + + update.old_pkgver = old_pkg.pkgver + update.old_pkgrel = old_pkg.pkgrel + update.old_epoch = old_pkg.epoch + + update.save(force_insert=True) + return update + + class Update(models.Model): package = models.ForeignKey(Package, related_name="updates", null=True, on_delete=models.SET_NULL) @@ -222,6 +256,8 @@ class Update(models.Model): new_pkgrel = models.CharField(max_length=255, null=True) new_epoch = models.PositiveIntegerField(null=True) + objects = UpdateManager() + class Meta: get_latest_by = 'created' -- cgit v1.2.3-54-g00ecf From 34e877d3328edd93b24bc82e63a89781d4350658 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 1 Jul 2012 20:35:34 -0500 Subject: Add indexes on 'created' field to several package-related models These models regularly sort by or limit by the created field, so adding a index on the created database column makes sense. Signed-off-by: Dan McGee --- packages/migrations/0018_create_created_indexes.py | 214 +++++++++++++++++++++ packages/models.py | 6 +- 2 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 packages/migrations/0018_create_created_indexes.py diff --git a/packages/migrations/0018_create_created_indexes.py b/packages/migrations/0018_create_created_indexes.py new file mode 100644 index 00000000..678a04d4 --- /dev/null +++ b/packages/migrations/0018_create_created_indexes.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.create_index('packages_flagrequest', ['created']) + db.create_index('packages_update', ['created']) + db.create_index('packages_signoff', ['created']) + + + def backwards(self, orm): + db.delete_index('packages_signoff', ['created']) + db.delete_index('packages_update', ['created']) + db.delete_index('packages_flagrequest', ['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',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + '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': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'", 'on_delete': 'models.PROTECT', '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': '2'}), + '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.depend': { + 'Meta': {'ordering': "['name']", 'object_name': 'Depend'}, + 'comparison': ('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'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'depends'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.flagrequest': { + 'Meta': {'object_name': 'FlagRequest'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'is_legitimate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'num_packages': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + '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', 'db_index': 'True'}), + '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'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'packages.update': { + 'Meta': {'object_name': 'Update'}, + 'action_flag': ('django.db.models.fields.PositiveSmallIntegerField', [], {}), + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Arch']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'new_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'new_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'old_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Package']"}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Repo']"}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index 04f35f9d..2f03a28b 100644 --- a/packages/models.py +++ b/packages/models.py @@ -138,7 +138,7 @@ class Signoff(models.Model): arch = models.ForeignKey(Arch) repo = models.ForeignKey(Repo) user = models.ForeignKey(User, related_name="package_signoffs") - created = models.DateTimeField(editable=False) + created = models.DateTimeField(editable=False, db_index=True) revoked = models.DateTimeField(null=True) comments = models.TextField(null=True, blank=True) @@ -170,7 +170,7 @@ class FlagRequest(models.Model): ''' user = models.ForeignKey(User, blank=True, null=True) user_email = models.EmailField('email address') - created = models.DateTimeField(editable=False) + created = models.DateTimeField(editable=False, db_index=True) # Great work, Django... https://code.djangoproject.com/ticket/18212 ip_address = models.GenericIPAddressField(verbose_name='IP address', unpack_ipv4=True) @@ -246,7 +246,7 @@ class Update(models.Model): pkgbase = models.CharField(max_length=255) action_flag = models.PositiveSmallIntegerField('action flag', choices=UPDATE_ACTION_CHOICES) - created = models.DateTimeField(editable=False) + created = models.DateTimeField(editable=False, db_index=True) old_pkgver = models.CharField(max_length=255, null=True) old_pkgrel = models.CharField(max_length=255, null=True) -- cgit v1.2.3-54-g00ecf From b88ef911b8c5e62b23ceb6d13d1a7a4f1b176f7f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 1 Jul 2012 20:42:24 -0500 Subject: Update flag out of date verbiage when orphans are involved From FS#29922, indicate what happens if the package is unmaintained. Signed-off-by: Dan McGee --- templates/packages/flaghelp.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/packages/flaghelp.html b/templates/packages/flaghelp.html index 399b7e01..c298b5c5 100644 --- a/templates/packages/flaghelp.html +++ b/templates/packages/flaghelp.html @@ -18,7 +18,8 @@

    Flagging Packages

    stable release available), then please notify us by using the Flag button in the Package Details screen. This will notify the maintainer(s) responsible for that - package so they can update it.

    + package so they can update it. If the package is unmaintained, the + notification will be sent to a developer mailing list.

    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 -- cgit v1.2.3-54-g00ecf From daf011b67a338f26ead8058a9f9caedfe251c62c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 5 Jul 2012 11:25:00 -0400 Subject: reporead: properly handle cases where last_update == files_last_update We should assume the filelists are up to date in this case, not out of date. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index 4e242af1..51c73c02 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -273,7 +273,7 @@ def populate_files(dbpkg, repopkg, force=False): return if not dbpkg.files_last_update or not dbpkg.last_update: pass - elif dbpkg.files_last_update > dbpkg.last_update: + elif dbpkg.files_last_update >= dbpkg.last_update: return # only delete files if we are reading a DB that contains them @@ -427,7 +427,7 @@ def filesonly_update(archname, reponame, pkgs, force=False): with transaction.commit_on_success(): if not dbpkg.files_last_update or not dbpkg.last_update: pass - elif not force and dbpkg.files_last_update > dbpkg.last_update: + elif not force and dbpkg.files_last_update >= dbpkg.last_update: logger.debug("Files for %s are up to date", pkg.name) continue dbpkg = Package.objects.select_for_update().get(id=dbpkg.id) -- cgit v1.2.3-54-g00ecf From 9d91cad678133e97345111fab2c103fcda9b9f28 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 5 Jul 2012 11:25:40 -0400 Subject: reporead: handle files in root directory properly Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index 51c73c02..df29a8a7 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -283,7 +283,10 @@ def populate_files(dbpkg, repopkg, force=False): len(repopkg.files), dbpkg.pkgname) pkg_files = [] for f in repopkg.files: - dirname, filename = f.rsplit('/', 1) + if '/' in f: + dirname, filename = f.rsplit('/', 1) + else: + dirname, filename = '', f if filename == '': filename = None pkgfile = PackageFile(pkg=dbpkg, -- cgit v1.2.3-54-g00ecf From 909cb9a209b4a4db00232b3a62656f95c4b88d45 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 5 Jul 2012 17:09:55 -0500 Subject: reporead: don't append slash to empty (root) directory Add the slash only if we have a directory name, and not otherwise. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index df29a8a7..43578d4a 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -285,13 +285,14 @@ def populate_files(dbpkg, repopkg, force=False): for f in repopkg.files: if '/' in f: dirname, filename = f.rsplit('/', 1) + dirname += '/' else: dirname, filename = '', f if filename == '': filename = None pkgfile = PackageFile(pkg=dbpkg, is_directory=(filename is None), - directory=dirname + '/', + directory=dirname, filename=filename) pkg_files.append(pkgfile) PackageFile.objects.bulk_create(pkg_files) -- cgit v1.2.3-54-g00ecf From ca86b8d339ff3b8e74ac6d2ccf8a14458a690cf5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 5 Jul 2012 17:36:22 -0500 Subject: Collapse the dependencies and required by lists when they are long For now, this happens when the lists are over 20 items. Using JS, hide the 21st and following packages listed in the list and replace them with a 'Show More...' link that users can click to get the full list. For a package such as glibc with 444 'Required By' entries, this can make quite a visual difference. Signed-off-by: Dan McGee --- sitestatic/archweb.js | 26 ++++++++++++++++++++++++-- templates/packages/details.html | 10 +++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/sitestatic/archweb.js b/sitestatic/archweb.js index 248be7a6..12b7c702 100644 --- a/sitestatic/archweb.js +++ b/sitestatic/archweb.js @@ -126,9 +126,9 @@ if (typeof $.tablesorter !== 'undefined') { (function($) { $.fn.enableCheckboxRangeSelection = function() { var lastCheckbox = null; - var lastElement = null; + var lastElement = null; - var spec = this; + var spec = this; spec.unbind("click.checkboxrange"); spec.bind("click.checkboxrange", function(e) { if (lastCheckbox != null && e.shiftKey) { @@ -170,6 +170,28 @@ function ajaxifyFiles() { }); } +function collapseDependsList(list) { + var limit = 20; + // hide everything past a given limit, but don't do anything if we don't + // enough items that it is worth adding the link + list = $(list); + var items = list.find('li').slice(limit); + if (items.length == 0) { + return; + } + items.hide(); + var linkid = list.attr('id') + 'link'; + list.after('

    Show More…

    '); + + // add links and wire them up to show the hidden items + $('#' + linkid).click(function(event) { + event.preventDefault(); + list.find('li').show(); + // remove the full

    node from the DOM + $(this).parent().remove(); + }); +} + /* packages/differences.html */ function filter_packages() { /* start with all rows, and then remove ones we shouldn't show */ diff --git a/templates/packages/details.html b/templates/packages/details.html index 358ab525..201e3074 100644 --- a/templates/packages/details.html +++ b/templates/packages/details.html @@ -186,7 +186,7 @@

    Versions Elsewhere

    Dependencies ({{deps|length}})

    - {% if deps %}
      + {% if deps %}
        {% for depend in deps %}{% include "packages/details_depend.html" %}{% endfor %}
      {% endif %}
    @@ -196,7 +196,7 @@

    Required By ({{rqdby|length}})

    - {% if rqdby %}
      + {% if rqdby %}
        {% for req in rqdby %}{% include "packages/details_requiredby.html" %}{% endfor %}
      {% endif %}
    @@ -219,6 +219,10 @@

    {% load cdn %}{% jquery %} {% endblock %} -- cgit v1.2.3-54-g00ecf From b95b0cd4197d70831754a7e81b40388c37ab1a3d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 8 Jul 2012 20:51:23 -0500 Subject: Use a set instead of list when gathering package IDs to fetch If we have duplicates in this list, it makes no sense to include them in the list we send to the database. Signed-off-by: Dan McGee --- packages/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils.py b/packages/utils.py index 82313472..b86b6eba 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -123,7 +123,7 @@ def get_differences_info(arch_a, arch_b): cursor.execute(sql, [arch_a.id, arch_b.id]) results = cursor.fetchall() # column A will always have a value, column B might be NULL - to_fetch = [row[0] for row in results] + to_fetch = set(row[0] for row in results) # fetch all of the necessary packages pkgs = Package.objects.normal().in_bulk(to_fetch) # now build a list of tuples containing differences -- cgit v1.2.3-54-g00ecf From a87da032cb6b5b84624e4205b5f8b7cab37249cd Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 8 Jul 2012 20:36:51 -0500 Subject: Handle HTTPException being thrown in mirrorcheck Managed to see this bubble up today when running the mirrorcheck command on a less than ideal connection that was experiencing timeouts at the wrong time. Signed-off-by: Dan McGee --- mirrors/management/commands/mirrorcheck.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mirrors/management/commands/mirrorcheck.py b/mirrors/management/commands/mirrorcheck.py index ae89d5e0..3d431796 100644 --- a/mirrors/management/commands/mirrorcheck.py +++ b/mirrors/management/commands/mirrorcheck.py @@ -11,6 +11,7 @@ from collections import deque from datetime import datetime +from httplib import HTTPException import logging import os from optparse import make_option @@ -115,6 +116,11 @@ def check_mirror_url(mirror_url, timeout): elif isinstance(e.reason, socket.error): log.error = e.reason.args[1] logger.debug("failed: %s, %s", url, log.error) + except HTTPException as e: + # e.g., BadStatusLine + log.is_success = False + log.error = "Exception in processing HTTP request." + logger.debug("failed: %s, %s", url, log.error) except socket.timeout as e: log.is_success = False log.error = "Connection timed out." -- cgit v1.2.3-54-g00ecf From 4cd588ae898c2abc8035cf0165ccdd038f2321bd Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 7 Jul 2012 16:50:06 -0500 Subject: Add triggers for adding package update rows This will be done instead of doing this logic at the application level, which has some subtle race conditions. When two simultaneous threads attempt to delete the same package, two update rows for the delete action are inserted. When done at the database level, we can ensure a one-to-one mapping between row operations and entries in this table. Signed-off-by: Dan McGee --- packages/sql/update.postgresql_psycopg2.sql | 45 +++++++++++++++++++++++++++++ packages/sql/update.sqlite3.sql | 30 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 packages/sql/update.postgresql_psycopg2.sql create mode 100644 packages/sql/update.sqlite3.sql diff --git a/packages/sql/update.postgresql_psycopg2.sql b/packages/sql/update.postgresql_psycopg2.sql new file mode 100644 index 00000000..6d678387 --- /dev/null +++ b/packages/sql/update.postgresql_psycopg2.sql @@ -0,0 +1,45 @@ +CREATE OR REPLACE FUNCTION packages_on_insert() RETURNS trigger AS $body$ +BEGIN + INSERT INTO packages_update + (action_flag, created, package_id, arch_id, repo_id, pkgname, pkgbase, new_pkgver, new_pkgrel, new_epoch) + VALUES (1, now(), NEW.id, NEW.arch_id, NEW.repo_id, NEW.pkgname, NEW.pkgbase, NEW.pkgver, NEW.pkgrel, NEW.epoch); + RETURN NULL; +END; +$body$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION packages_on_update() RETURNS trigger AS $body$ +BEGIN + INSERT INTO packages_update + (action_flag, created, package_id, arch_id, repo_id, pkgname, pkgbase, old_pkgver, old_pkgrel, old_epoch, new_pkgver, new_pkgrel, new_epoch) + VALUES (2, now(), NEW.id, NEW.arch_id, NEW.repo_id, NEW.pkgname, NEW.pkgbase, OLD.pkgver, OLD.pkgrel, OLD.epoch, NEW.pkgver, NEW.pkgrel, NEW.epoch); + RETURN NULL; +END; +$body$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION packages_on_delete() RETURNS trigger AS $body$ +BEGIN + INSERT INTO packages_update + (action_flag, created, arch_id, repo_id, pkgname, pkgbase, old_pkgver, old_pkgrel, old_epoch) + VALUES (3, now(), OLD.arch_id, OLD.repo_id, OLD.pkgname, OLD.pkgbase, OLD.pkgver, OLD.pkgrel, OLD.epoch); + RETURN NULL; +END; +$body$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS packages_insert ON packages; +CREATE TRIGGER packages_insert + AFTER INSERT ON packages + FOR EACH ROW + EXECUTE PROCEDURE packages_on_insert(); + +DROP TRIGGER IF EXISTS packages_update ON packages; +CREATE TRIGGER packages_update + AFTER UPDATE ON packages + FOR EACH ROW + WHEN (OLD.pkgver != NEW.pkgver OR OLD.pkgrel != NEW.pkgrel OR OLD.epoch != NEW.epoch) + EXECUTE PROCEDURE packages_on_update(); + +DROP TRIGGER IF EXISTS packages_delete ON packages; +CREATE TRIGGER packages_delete + AFTER DELETE ON packages + FOR EACH ROW + EXECUTE PROCEDURE packages_on_delete(); diff --git a/packages/sql/update.sqlite3.sql b/packages/sql/update.sqlite3.sql new file mode 100644 index 00000000..6f151bdd --- /dev/null +++ b/packages/sql/update.sqlite3.sql @@ -0,0 +1,30 @@ +DROP TRIGGER IF EXISTS packages_insert; +CREATE TRIGGER packages_insert + AFTER INSERT ON packages + FOR EACH ROW + BEGIN + INSERT INTO packages_update + (action_flag, created, package_id, arch_id, repo_id, pkgname, pkgbase, new_pkgver, new_pkgrel, new_epoch) + VALUES (1, strftime('%Y-%m-%d %H:%M:%f', 'now'), NEW.id, NEW.arch_id, NEW.repo_id, NEW.pkgname, NEW.pkgbase, NEW.pkgver, NEW.pkgrel, NEW.epoch); + END; + +DROP TRIGGER IF EXISTS packages_update; +CREATE TRIGGER packages_update + AFTER UPDATE ON packages + FOR EACH ROW + WHEN (OLD.pkgver != NEW.pkgver OR OLD.pkgrel != NEW.pkgrel OR OLD.epoch != NEW.epoch) + BEGIN + INSERT INTO packages_update + (action_flag, created, package_id, arch_id, repo_id, pkgname, pkgbase, old_pkgver, old_pkgrel, old_epoch, new_pkgver, new_pkgrel, new_epoch) + VALUES (2, strftime('%Y-%m-%d %H:%M:%f', 'now'), NEW.id, NEW.arch_id, NEW.repo_id, NEW.pkgname, NEW.pkgbase, OLD.pkgver, OLD.pkgrel, OLD.epoch, NEW.pkgver, NEW.pkgrel, NEW.epoch); + END; + +DROP TRIGGER IF EXISTS packages_delete; +CREATE TRIGGER packages_delete + AFTER DELETE ON packages + FOR EACH ROW + BEGIN + INSERT INTO packages_update + (action_flag, created, arch_id, repo_id, pkgname, pkgbase, old_pkgver, old_pkgrel, old_epoch) + VALUES (3, strftime('%Y-%m-%d %H:%M:%f', 'now'), OLD.arch_id, OLD.repo_id, OLD.pkgname, OLD.pkgbase, OLD.pkgver, OLD.pkgrel, OLD.epoch); + END; -- cgit v1.2.3-54-g00ecf From 26a00cadcebc0b37775954d261ec73f927ceca12 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 7 Jul 2012 17:28:02 -0500 Subject: Don't log package updates in Python when we have DB trigger support This adds a helper method to find the database engine in use, and then skips code we shouldn't execute if we are doing this another way. Note that this helper method could be useful for backend-specific code paths elsewhere, such as custom SQL being called or lack of StdDev() in sqlite3 out of the box. Signed-off-by: Dan McGee --- main/utils.py | 11 +++++++++++ packages/models.py | 15 +++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/main/utils.py b/main/utils.py index b7cb19f4..879abfb9 100644 --- a/main/utils.py +++ b/main/utils.py @@ -8,6 +8,7 @@ from pytz import utc from django.core.cache import cache +from django.db import connections, router CACHE_TIMEOUT = 1800 @@ -106,6 +107,16 @@ def set_created_field(sender, **kwargs): obj.created = utc_now() +def database_vendor(model, mode='read'): + if mode == 'read': + database = router.db_for_read(model) + elif mode == 'write': + database = router.db_for_write(model) + else: + raise Exception('Invalid database mode specified') + return connections[database].vendor + + def groupby_preserve_order(iterable, keyfunc): '''Take an iterable and regroup using keyfunc to determine whether items belong to the same group. The order of the iterable is preserved and diff --git a/packages/models.py b/packages/models.py index 2f03a28b..45ff3c08 100644 --- a/packages/models.py +++ b/packages/models.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import User from main.models import Arch, Repo, Package -from main.utils import set_created_field +from main.utils import set_created_field, database_vendor class PackageRelation(models.Model): @@ -207,7 +207,12 @@ class UpdateManager(models.Manager): def log_update(self, old_pkg, new_pkg): '''Utility method to help log an update. This will determine the type based on how many packages are passed in, and will pull the relevant - necesary fields off the given packages.''' + necesary fields off the given packages. + Note that in some cases, this is a no-op if we know this database type + supports triggers to add these rows instead.''' + if database_vendor(Package, 'write') in ('sqlite', 'postgresql'): + # we log updates using database triggers for these backends + return update = Update() if new_pkg: update.action_flag = ADDITION @@ -222,6 +227,12 @@ def log_update(self, old_pkg, new_pkg): if old_pkg: if new_pkg: update.action_flag = CHANGE + # ensure we should even be logging this + if (old_pkg.pkgver == new_pkg.pkgver and + old_pkg.pkgrel == new_pkg.pkgrel and + old_pkg.epoch == new_pkg.epoch): + # all relevant fields were the same; e.g. a force update + return else: update.action_flag = DELETION update.arch = old_pkg.arch -- cgit v1.2.3-54-g00ecf From 0f3c894e7a0f573fa0198459150f387c3a7f23ae Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 8 Jul 2012 20:38:01 -0500 Subject: Don't include StdDev on sqlite3 mirror status query Because this function isn't shipped by default, it makes more sense to just omit it completely from the query we do to build the tables on this page when in development. Substitute 0.0 for the value so the rest of the calculations and display work as expected. Signed-off-by: Dan McGee --- mirrors/utils.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mirrors/utils.py b/mirrors/utils.py index 2014411d..9aa8e0f5 100644 --- a/mirrors/utils.py +++ b/mirrors/utils.py @@ -3,7 +3,7 @@ from django.db.models import Avg, Count, Max, Min, StdDev from django_countries.fields import Country -from main.utils import cache_function, utc_now +from main.utils import cache_function, utc_now, database_vendor from .models import MirrorLog, MirrorProtocol, MirrorUrl @@ -40,8 +40,11 @@ def get_mirror_statuses(cutoff=default_cutoff): success_count=Count('logs__duration'), last_sync=Max('logs__last_sync'), last_check=Max('logs__check_time'), - duration_avg=Avg('logs__duration'), - duration_stddev=StdDev('logs__duration')) + duration_avg=Avg('logs__duration')) + + vendor = database_vendor(MirrorUrl) + if vendor != 'sqlite': + urls.annotate(duration_stddev=StdDev('logs__duration')) # The Django ORM makes it really hard to get actual average delay in the # above query, so run a seperate query for it and we will process the @@ -70,6 +73,9 @@ def get_mirror_statuses(cutoff=default_cutoff): check_frequency = None for url in urls: + # fake the standard deviation for local testing setups + if vendor == 'sqlite': + setattr(url, 'duration_stddev', 0.0) annotate_url(url, delays) return { -- cgit v1.2.3-54-g00ecf From 3c906888e2ba9e55cef00dfc61667fb383c9754d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 8 Jul 2012 20:44:07 -0500 Subject: Get multilib package differences query working on sqlite3 Thank you database engines for all implementing such simple operations as substring() and length() in different ways. Signed-off-by: Dan McGee --- packages/utils.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/utils.py b/packages/utils.py index b86b6eba..6d54d71a 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -8,7 +8,8 @@ from django.contrib.auth.models import User from main.models import Package, PackageFile, Arch, Repo -from main.utils import cache_function, groupby_preserve_order, PackageStandin +from main.utils import (cache_function, database_vendor, + groupby_preserve_order, PackageStandin) from .models import (PackageGroup, PackageRelation, License, Depend, Conflict, Provision, Replacement, SignoffSpecification, Signoff, DEFAULT_SIGNOFF_SPEC) @@ -150,12 +151,18 @@ def get_differences_info(arch_a, arch_b): def multilib_differences(): # Query for checking multilib out of date-ness - sql = """ -SELECT ml.id, reg.id - FROM packages ml - JOIN packages reg - ON ( - reg.pkgname = ( + if database_vendor(Package) == 'sqlite': + pkgname_sql = """ + CASE WHEN ml.pkgname LIKE %s + THEN SUBSTR(ml.pkgname, 7) + WHEN ml.pkgname LIKE %s + THEN SUBSTR(ml.pkgname, 1, LENGTH(ml.pkgname) - 9) + ELSE + ml.pkgname + END + """ + else: + pkgname_sql = """ CASE WHEN ml.pkgname LIKE %s THEN SUBSTRING(ml.pkgname, 7) WHEN ml.pkgname LIKE %s @@ -163,7 +170,13 @@ def multilib_differences(): ELSE ml.pkgname END - ) + """ + sql = """ +SELECT ml.id, reg.id + FROM packages ml + JOIN packages reg + ON ( + reg.pkgname = (""" + pkgname_sql + """) AND reg.pkgver != ml.pkgver ) JOIN repos r ON reg.repo_id = r.id @@ -172,7 +185,7 @@ def multilib_differences(): AND r.staging = %s AND reg.arch_id = %s ORDER BY ml.last_update -""" + """ multilib = Repo.objects.get(name__iexact='multilib') i686 = Arch.objects.get(name='i686') params = ['lib32-%', '%-multilib', multilib.id, False, False, i686.id] -- cgit v1.2.3-54-g00ecf From a1ec14fc68282d67c00c79b5aa6aab60461f056a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 12 Mar 2012 13:14:49 -0400 Subject: reporead: disable FULL synchronous writes for sqlite3 At least on Linux, we hit a huge bottleneck waiting for the FULL commit to happen for each added package during reporead operations. It makes much more sense to back this off to FULL level instead, which trades some possible loss of durability for speedier operation. Additionally, no one would possibly be running their production version of this site on sqlite3, right? Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index 43578d4a..e50686b1 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -547,6 +547,12 @@ def read_repo(primary_arch, repo_file, options): package.name, repo_file, package.arch)) del packages + database = router.db_for_write(Package) + connection = connections[database] + if connection.vendor == 'sqlite': + cursor = connection.cursor() + cursor.execute('PRAGMA synchronous = NORMAL') + logger.info('Starting database updates for %s.', repo_file) for arch in sorted(packages_arches.keys()): if filesonly: -- cgit v1.2.3-54-g00ecf From 3c4ceb16331b37fd334dc9682d4cde6430838942 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 8 Jul 2012 20:56:28 -0500 Subject: mirrorcheck: Don't use bulk_create on sqlite3 It isn't worth it, as we run into the 999 max SQL statement variables issue when using it on any significant amount of mirrors. Since this is just a development database setup, and it isn't a command we need to run especially fast, we can ditch it. Signed-off-by: Dan McGee --- mirrors/management/commands/mirrorcheck.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mirrors/management/commands/mirrorcheck.py b/mirrors/management/commands/mirrorcheck.py index 3d431796..7a133cbf 100644 --- a/mirrors/management/commands/mirrorcheck.py +++ b/mirrors/management/commands/mirrorcheck.py @@ -30,7 +30,7 @@ from django.core.management.base import NoArgsCommand from django.db import transaction -from main.utils import utc_now +from main.utils import utc_now, database_vendor from mirrors.models import MirrorUrl, MirrorLog logging.basicConfig( @@ -207,7 +207,11 @@ def run(self): logger.debug("joining on all threads") self.tasks.join() logger.debug("processing %d log entries", len(self.logs)) - MirrorLog.objects.bulk_create(self.logs) + if database_vendor(MirrorLog, mode='write') == 'sqlite': + for log in self.logs: + log.save(force_insert=True) + else: + MirrorLog.objects.bulk_create(self.logs) logger.debug("log entries saved") # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From ef8fb7c7f242fc0f081f86d283e2fac22504a6b5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 8 Jul 2012 21:24:36 -0500 Subject: Make collapseDependsList() a bit smarter Signed-off-by: Dan McGee --- sitestatic/archweb.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sitestatic/archweb.js b/sitestatic/archweb.js index 12b7c702..01d5b269 100644 --- a/sitestatic/archweb.js +++ b/sitestatic/archweb.js @@ -172,18 +172,18 @@ function ajaxifyFiles() { function collapseDependsList(list) { var limit = 20; - // hide everything past a given limit, but don't do anything if we don't - // enough items that it is worth adding the link list = $(list); + // Hide everything past a given limit. Don't do anything if we don't have + // enough items, or the link already exists. + var linkid = list.attr('id') + 'link'; var items = list.find('li').slice(limit); - if (items.length == 0) { + if (items.length == 0 || $('#' + linkid).length > 0) { return; } items.hide(); - var linkid = list.attr('id') + 'link'; list.after('

    Show More…

    '); - // add links and wire them up to show the hidden items + // add link and wire it up to show the hidden items $('#' + linkid).click(function(event) { event.preventDefault(); list.find('li').show(); -- cgit v1.2.3-54-g00ecf From 88ee61a39ac3690267f2b7903f3646972e8f055d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 8 Jul 2012 21:24:48 -0500 Subject: Work around bulk_create limitations in sqlite3 in reporead Given the 999 SQL statement variable limit, we can easily hit it when updating a package with thousands of files or a few hundred depends. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index e50686b1..2d9b68b2 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -31,7 +31,7 @@ from devel.utils import UserFinder from main.models import Arch, Package, PackageFile, Repo -from main.utils import utc_now +from main.utils import utc_now, database_vendor from packages.models import Depend, Conflict, Provision, Replacement, Update @@ -184,6 +184,28 @@ def create_related(model, package, rel_str, equals_only=False): return None return related + +def batched_bulk_create(model, all_objects): + # for short lists, just bulk_create as we should be fine + if len(all_objects) < 20: + return model.objects.bulk_create(all_objects) + + if database_vendor(model, mode='write') == 'sqlite': + # 999 max variables in each SQL statement + incr = 999 // len(model._meta.fields) + else: + incr = 1000 + + def chunks(): + offset = 0 + while offset < len(all_objects): + yield all_objects[offset:offset + incr] + offset += incr + + for items in chunks(): + model.objects.bulk_create(items) + + def create_multivalued(dbpkg, repopkg, db_attr, repo_attr): '''Populate the simplest of multivalued attributes. These are those that only deal with a 'name' attribute, such as licenses, groups, etc. The input @@ -235,20 +257,20 @@ def populate_pkg(dbpkg, repopkg, force=False, timestamp=None): dbpkg.depends.all().delete() deps = [create_depend(dbpkg, y) for y in repopkg.depends] deps += [create_depend(dbpkg, y, True) for y in repopkg.optdepends] - Depend.objects.bulk_create(deps) + batched_bulk_create(Depend, deps) dbpkg.conflicts.all().delete() conflicts = [create_related(Conflict, dbpkg, y) for y in repopkg.conflicts] - Conflict.objects.bulk_create(conflicts) + batched_bulk_create(Conflict, conflicts) dbpkg.provides.all().delete() provides = [create_related(Provision, dbpkg, y, equals_only=True) for y in repopkg.provides] - Provision.objects.bulk_create(provides) + batched_bulk_create(Provision, provides) dbpkg.replaces.all().delete() replaces = [create_related(Replacement, dbpkg, y) for y in repopkg.replaces] - Replacement.objects.bulk_create(replaces) + batched_bulk_create(Replacement, replaces) create_multivalued(dbpkg, repopkg, 'groups', 'groups') create_multivalued(dbpkg, repopkg, 'licenses', 'license') @@ -295,7 +317,7 @@ def populate_files(dbpkg, repopkg, force=False): directory=dirname, filename=filename) pkg_files.append(pkgfile) - PackageFile.objects.bulk_create(pkg_files) + batched_bulk_create(PackageFile, pkg_files) dbpkg.files_last_update = utc_now() dbpkg.save() -- cgit v1.2.3-54-g00ecf From d7c0ae9b555ff45d0cbaca09f4f427c7e1758d4f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 8 Jul 2012 21:28:34 -0500 Subject: Add tags file (ctags) to .gitignore Also add 'env/', a directory I frequently use for the virtualenv. Signed-off-by: Dan McGee --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e6378211..a089ddd8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,7 @@ local_settings.py archweb.db archweb.db-* +tags collected_static/ testing/ +env/ -- cgit v1.2.3-54-g00ecf From 9c7350650e66b5eb6228778e335a160be5ea7f98 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 8 Jul 2012 21:53:35 -0500 Subject: Correctly reassign queryset with added annotation in mirror status This was a dumb oversight on my part in commit 0f3c894e7a0. Signed-off-by: Dan McGee --- mirrors/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mirrors/utils.py b/mirrors/utils.py index 9aa8e0f5..f2c98ee0 100644 --- a/mirrors/utils.py +++ b/mirrors/utils.py @@ -44,7 +44,7 @@ def get_mirror_statuses(cutoff=default_cutoff): vendor = database_vendor(MirrorUrl) if vendor != 'sqlite': - urls.annotate(duration_stddev=StdDev('logs__duration')) + urls = urls.annotate(duration_stddev=StdDev('logs__duration')) # The Django ORM makes it really hard to get actual average delay in the # above query, so run a seperate query for it and we will process the -- cgit v1.2.3-54-g00ecf From c589f7d930ee7642524ecc33ee41bae79d4edd9b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 8 Jul 2012 23:24:35 -0500 Subject: Don't remove approval CSS class when updating signoff list Signed-off-by: Dan McGee --- sitestatic/archweb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sitestatic/archweb.js b/sitestatic/archweb.js index 01d5b269..c0fb3a60 100644 --- a/sitestatic/archweb.js +++ b/sitestatic/archweb.js @@ -288,7 +288,7 @@ function signoff_package() { } /* update the approved column to reflect reality */ var approved = link.closest('tr').children('.approval'); - approved.attr('class', ''); + approved.attr('class', 'approval'); if (data.known_bad) { approved.text('Bad').addClass('signoff-bad'); } else if (!data.enabled) { -- cgit v1.2.3-54-g00ecf From 8383a071608329c7683f7a710030ce945bd20b4d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 8 Jul 2012 23:30:48 -0500 Subject: Add a new jquery_tablesorter CDN template tag And use it everywhere we were including the file before. This should make updating the version a heck of a lot easier. Signed-off-by: Dan McGee --- main/templatetags/cdn.py | 7 +++++++ templates/devel/clock.html | 3 +-- templates/devel/index.html | 3 +-- templates/devel/packages.html | 3 +-- templates/mirrors/mirror_details.html | 3 +-- templates/mirrors/mirrors.html | 3 +-- templates/mirrors/status.html | 3 +-- templates/packages/differences.html | 3 +-- templates/packages/groups.html | 3 +-- templates/packages/packages_list.html | 3 +-- templates/packages/signoffs.html | 3 +-- templates/packages/stale_relations.html | 3 +-- templates/public/keys.html | 3 +-- templates/releng/iso_overview.html | 3 +-- templates/releng/result_list.html | 3 +-- templates/todolists/list.html | 3 +-- templates/todolists/public_list.html | 3 +-- templates/todolists/view.html | 3 +-- 18 files changed, 24 insertions(+), 34 deletions(-) diff --git a/main/templatetags/cdn.py b/main/templatetags/cdn.py index ab5d881a..54299823 100644 --- a/main/templatetags/cdn.py +++ b/main/templatetags/cdn.py @@ -17,4 +17,11 @@ def jquery(): link = staticfiles_storage.url(filename) return '' % link + +@register.simple_tag +def jquery_tablesorter(): + filename = 'jquery.tablesorter.min.js' + link = staticfiles_storage.url(filename) + return '' % link + # vim: set ts=4 sw=4 et: diff --git a/templates/devel/clock.html b/templates/devel/clock.html index 6a3f0a69..9ebea06c 100644 --- a/templates/devel/clock.html +++ b/templates/devel/clock.html @@ -52,8 +52,7 @@

    Developer World Clocks

    Developer Username AliasLast Action Location Time Zone Current Time {{ dev.get_full_name }} {{ dev.username }} {{ dev.userprofile.alias }}{{ dev.last_action }} {% if dev.userprofile.country %}{{ dev.userprofile.country.name }} {% endif %}{{ dev.userprofile.location }} {{ dev.userprofile.time_zone }} {{ utc_now|timezone:dev.userprofile.time_zone|date:"Y-m-d H:i T" }} {{ dev.userprofile.time_zone }} {% with pkg.signer as signer %}{% if signer %}{% pgp_key_link pkg.signature.key_id signer.get_full_name %}{% else %}Unknown ({% pgp_key_link pkg.signature.key_id %}){% endif %}{% endwith %}
    Signature Date:{{ pkg.signature.datetime|date:"DATETIME_FORMAT" }} UTC{{ pkg.signature.creation_time|date:"DATETIME_FORMAT" }} UTC
    Signed By: Unsigned{{ log.url__url }} {{ log.url__protocol__protocol }} {% if log.country %} {% endif %}{{ log.country.name }}{{ log.error }}{{ log.error|linebreaksbr }} {{ log.last_occurred|date:'Y-m-d H:i' }} {{ log.error_count }}
    Provides:{{ provides|join:", " }}{% include "packages/details_relatedto.html" %}
    Conflicts:{{ conflicts|join:", " }}{% include "packages/details_relatedto.html" %}
    Replaces:{{ replaces|join:", " }}{% include "packages/details_relatedto.html" %}
    {% if option.is_rollback %}Rollback: {% endif %}{{ option.name|title }} + {% if option.is_rollback %}Rollback: {% endif %}{{ option.name|title }} Last Success Last Failure
    -{% load cdn %}{% jquery %} - +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} +{% load cdn %}{% jquery %}{% jquery_tablesorter %} {% endblock %} -- cgit v1.2.3-54-g00ecf From 61b4098c611592d62b40a9ee941976a869dff4fc Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 24 Jul 2012 08:55:48 -0500 Subject: Add index on package updates pkgname field Signed-off-by: Dan McGee --- .../0019_package_update_pkgname_index.py | 208 +++++++++++++++++++++ packages/models.py | 2 +- 2 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 packages/migrations/0019_package_update_pkgname_index.py diff --git a/packages/migrations/0019_package_update_pkgname_index.py b/packages/migrations/0019_package_update_pkgname_index.py new file mode 100644 index 00000000..047f11ec --- /dev/null +++ b/packages/migrations/0019_package_update_pkgname_index.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.create_index('packages_update', ['pkgname']) + + def backwards(self, orm): + db.delete_index('packages_update', ['pkgname']) + + 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',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + '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': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'", 'on_delete': 'models.PROTECT', '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': '2'}), + '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.depend': { + 'Meta': {'ordering': "['name']", 'object_name': 'Depend'}, + 'comparison': ('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'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'depends'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.flagrequest': { + 'Meta': {'object_name': 'FlagRequest'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'is_legitimate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'num_packages': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + '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', 'db_index': 'True'}), + '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'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'packages.update': { + 'Meta': {'object_name': 'Update'}, + 'action_flag': ('django.db.models.fields.PositiveSmallIntegerField', [], {}), + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Arch']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'new_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'new_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'old_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Package']"}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Repo']"}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index 18a62a3c..5b48b30f 100644 --- a/packages/models.py +++ b/packages/models.py @@ -253,7 +253,7 @@ class Update(models.Model): null=True, on_delete=models.SET_NULL) repo = models.ForeignKey(Repo, related_name="updates") arch = models.ForeignKey(Arch, related_name="updates") - pkgname = models.CharField(max_length=255) + pkgname = models.CharField(max_length=255, db_index=True) pkgbase = models.CharField(max_length=255) action_flag = models.PositiveSmallIntegerField('action flag', choices=UPDATE_ACTION_CHOICES) -- cgit v1.2.3-54-g00ecf From c0bf9e20660cfae7ea8994472555bba23398b598 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 24 Jul 2012 09:19:48 -0500 Subject: Remove custom utc_now() function, use django.utils.timezone.now() This was around from the time when we handled timezones sanely and Django did not; now that we are on 1.4 we no longer need our own code to handle this. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 9 +++++---- devel/models.py | 5 +++-- devel/views.py | 12 ++++++------ main/models.py | 5 +++-- main/utils.py | 9 ++------- mirrors/management/commands/mirrorcheck.py | 7 ++++--- mirrors/utils.py | 17 +++++++++-------- news/models.py | 11 +++++------ packages/management/commands/signoff_report.py | 8 ++++---- packages/views/flag.py | 8 ++++---- packages/views/signoff.py | 4 ++-- releng/management/commands/syncisos.py | 5 ++--- 12 files changed, 49 insertions(+), 51 deletions(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index 2d9b68b2..e69691db 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -28,10 +28,11 @@ from django.core.management.base import BaseCommand, CommandError from django.db import connections, router, transaction from django.db.utils import IntegrityError +from django.utils.timezone import now from devel.utils import UserFinder from main.models import Arch, Package, PackageFile, Repo -from main.utils import utc_now, database_vendor +from main.utils import database_vendor from packages.models import Depend, Conflict, Provision, Replacement, Update @@ -318,7 +319,7 @@ def populate_files(dbpkg, repopkg, force=False): filename=filename) pkg_files.append(pkgfile) batched_bulk_create(PackageFile, pkg_files) - dbpkg.files_last_update = utc_now() + dbpkg.files_last_update = now() dbpkg.save() @@ -388,7 +389,7 @@ def db_update(archname, reponame, pkgs, force=False): dbpkg = Package(pkgname=pkg.name, arch=architecture, repo=repository) try: with transaction.commit_on_success(): - populate_pkg(dbpkg, pkg, timestamp=utc_now()) + populate_pkg(dbpkg, pkg, timestamp=now()) Update.objects.log_update(None, dbpkg) except IntegrityError: logger.warning("Could not add package %s; " @@ -417,7 +418,7 @@ def db_update(archname, reponame, pkgs, force=False): if not force and pkg_same_version(pkg, dbpkg): continue elif not force: - timestamp = utc_now() + timestamp = now() # The odd select_for_update song and dance here are to ensure # simultaneous updates don't happen on a package, causing diff --git a/devel/models.py b/devel/models.py index fd5df00a..9b6f07a7 100644 --- a/devel/models.py +++ b/devel/models.py @@ -4,10 +4,11 @@ from django.db import models from django.db.models.signals import pre_save from django.contrib.auth.models import User +from django.utils.timezone import now from django_countries import CountryField from .fields import PGPKeyField -from main.utils import make_choice, utc_now +from main.utils import make_choice class UserProfile(models.Model): @@ -105,7 +106,7 @@ def set_last_modified(sender, **kwargs): signal handler.''' obj = kwargs['instance'] if hasattr(obj, 'last_modified'): - obj.last_modified = utc_now() + obj.last_modified = now() # connect signals needed to keep cache in line with reality diff --git a/devel/views.py b/devel/views.py index 78ed26f2..143b12bf 100644 --- a/devel/views.py +++ b/devel/views.py @@ -24,11 +24,11 @@ from django.views.generic.simple import direct_to_template from django.utils.encoding import force_unicode from django.utils.http import http_date +from django.utils.timezone import now from .models import UserProfile from main.models import Package, PackageFile, TodolistPkg from main.models import Arch, Repo -from main.utils import utc_now from news.models import News from packages.models import PackageRelation, Signoff, Depend from packages.utils import get_signoff_groups @@ -122,15 +122,15 @@ def clock(request): else: dev.last_action = None - now = utc_now() + current_time = now() page_dict = { 'developers': devs, - 'utc_now': now, + 'utc_now': current_time, } response = direct_to_template(request, 'devel/clock.html', page_dict) if not response.has_header('Expires'): - expire_time = now.replace(second=0, microsecond=0) + expire_time = current_time.replace(second=0, microsecond=0) expire_time += timedelta(minutes=1) expire_time = time.mktime(expire_time.timetuple()) response['Expires'] = http_date(expire_time) @@ -198,12 +198,12 @@ def report(request, report_name, username=None): if report_name == 'old': title = 'Packages last built more than one year ago' - cutoff = utc_now() - timedelta(days=365) + cutoff = now() - timedelta(days=365) packages = packages.filter( build_date__lt=cutoff).order_by('build_date') elif report_name == 'long-out-of-date': title = 'Packages marked out-of-date more than 90 days ago' - cutoff = utc_now() - timedelta(days=90) + cutoff = now() - timedelta(days=90) packages = packages.filter( flag_date__lt=cutoff).order_by('flag_date') elif report_name == 'big': diff --git a/main/models.py b/main/models.py index 04d8da8f..6c9dfe4d 100644 --- a/main/models.py +++ b/main/models.py @@ -6,9 +6,10 @@ from django.db import models from django.contrib.auth.models import User from django.contrib.sites.models import Site +from django.utils.timezone import now from .fields import PositiveBigIntegerField -from .utils import cache_function, set_created_field, utc_now +from .utils import cache_function, set_created_field class TodolistManager(models.Manager): @@ -385,7 +386,7 @@ class Meta: def set_todolist_fields(sender, **kwargs): todolist = kwargs['instance'] if not todolist.date_added: - todolist.date_added = utc_now() + todolist.date_added = now() # connect signals needed to keep cache in line with reality from main.utils import refresh_latest diff --git a/main/utils.py b/main/utils.py index 879abfb9..0b6849a4 100644 --- a/main/utils.py +++ b/main/utils.py @@ -5,10 +5,10 @@ from datetime import datetime import hashlib -from pytz import utc from django.core.cache import cache from django.db import connections, router +from django.utils.timezone import now CACHE_TIMEOUT = 1800 @@ -94,17 +94,12 @@ def retrieve_latest(sender, latest_by=None): return None -def utc_now(): - '''Returns a timezone-aware UTC date representing now.''' - return datetime.utcnow().replace(tzinfo=utc) - - def set_created_field(sender, **kwargs): '''This will set the 'created' field on any object to the current UTC time if it is unset. For use as a pre_save signal handler.''' obj = kwargs['instance'] if hasattr(obj, 'created') and not obj.created: - obj.created = utc_now() + obj.created = now() def database_vendor(model, mode='read'): diff --git a/mirrors/management/commands/mirrorcheck.py b/mirrors/management/commands/mirrorcheck.py index 7a133cbf..e09ea680 100644 --- a/mirrors/management/commands/mirrorcheck.py +++ b/mirrors/management/commands/mirrorcheck.py @@ -29,8 +29,9 @@ from django.core.management.base import NoArgsCommand from django.db import transaction +from django.utils.timezone import now -from main.utils import utc_now, database_vendor +from main.utils import database_vendor from mirrors.models import MirrorUrl, MirrorLog logging.basicConfig( @@ -83,7 +84,7 @@ def parse_lastsync(log, data): def check_mirror_url(mirror_url, timeout): url = mirror_url.url + 'lastsync' logger.info("checking URL %s", url) - log = MirrorLog(url=mirror_url, check_time=utc_now()) + log = MirrorLog(url=mirror_url, check_time=now()) headers = {'User-Agent': 'archweb/1.0'} req = urllib2.Request(url, None, headers) try: @@ -136,7 +137,7 @@ def check_mirror_url(mirror_url, timeout): def check_rsync_url(mirror_url, timeout): url = mirror_url.url + 'lastsync' logger.info("checking URL %s", url) - log = MirrorLog(url=mirror_url, check_time=utc_now()) + log = MirrorLog(url=mirror_url, check_time=now()) tempdir = tempfile.mkdtemp() lastsync_path = os.path.join(tempdir, 'lastsync') diff --git a/mirrors/utils.py b/mirrors/utils.py index f2c98ee0..bf030d39 100644 --- a/mirrors/utils.py +++ b/mirrors/utils.py @@ -1,13 +1,14 @@ from datetime import timedelta from django.db.models import Avg, Count, Max, Min, StdDev +from django.utils.timezone import now from django_countries.fields import Country -from main.utils import cache_function, utc_now, database_vendor +from main.utils import cache_function, database_vendor from .models import MirrorLog, MirrorProtocol, MirrorUrl -default_cutoff = timedelta(hours=24) +DEFAULT_CUTOFF = timedelta(hours=24) def annotate_url(url, delays): '''Given a MirrorURL object, add a few more attributes to it regarding @@ -30,8 +31,8 @@ def annotate_url(url, delays): @cache_function(123) -def get_mirror_statuses(cutoff=default_cutoff): - cutoff_time = utc_now() - cutoff +def get_mirror_statuses(cutoff=DEFAULT_CUTOFF): + cutoff_time = now() - cutoff # I swear, this actually has decent performance... urls = MirrorUrl.objects.select_related('mirror', 'protocol').filter( mirror__active=True, mirror__public=True, @@ -88,8 +89,8 @@ def get_mirror_statuses(cutoff=default_cutoff): @cache_function(117) -def get_mirror_errors(cutoff=default_cutoff): - cutoff_time = utc_now() - cutoff +def get_mirror_errors(cutoff=DEFAULT_CUTOFF): + cutoff_time = now() - cutoff errors = MirrorLog.objects.filter( is_success=False, check_time__gte=cutoff_time, url__mirror__active=True, url__mirror__public=True).values( @@ -105,11 +106,11 @@ def get_mirror_errors(cutoff=default_cutoff): @cache_function(295) -def get_mirror_url_for_download(cutoff=default_cutoff): +def get_mirror_url_for_download(cutoff=DEFAULT_CUTOFF): '''Find a good mirror URL to use for package downloads. If we have mirror status data available, it is used to determine a good choice by looking at the last batch of status rows.''' - cutoff_time = utc_now() - cutoff + cutoff_time = now() - cutoff status_data = MirrorLog.objects.filter( check_time__gte=cutoff_time).aggregate( Max('check_time'), Max('last_sync')) diff --git a/news/models.py b/news/models.py index 95026e1d..2efea579 100644 --- a/news/models.py +++ b/news/models.py @@ -1,8 +1,7 @@ from django.db import models from django.contrib.auth.models import User from django.contrib.sites.models import Site - -from main.utils import utc_now +from django.utils.timezone import now class News(models.Model): @@ -29,13 +28,13 @@ class Meta: def set_news_fields(sender, **kwargs): news = kwargs['instance'] - now = utc_now() - news.last_modified = now + current_time = now() + news.last_modified = current_time if not news.postdate: - news.postdate = now + news.postdate = current_time # 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()) + current_time.strftime('%Y-%m-%d'), news.get_absolute_url()) # connect signals needed to keep cache in line with reality from main.utils import refresh_latest diff --git a/packages/management/commands/signoff_report.py b/packages/management/commands/signoff_report.py index ddf930db..72fcbe1e 100644 --- a/packages/management/commands/signoff_report.py +++ b/packages/management/commands/signoff_report.py @@ -15,6 +15,7 @@ from django.contrib.sites.models import Site from django.db.models import Count from django.template import loader, Context +from django.utils.timezone import now from collections import namedtuple from datetime import timedelta @@ -23,7 +24,6 @@ import sys from main.models import Repo -from main.utils import utc_now from packages.models import Signoff from packages.utils import get_signoff_groups @@ -66,9 +66,9 @@ def generate_report(email, repo_name): new_hours = 24 old_days = 14 - now = utc_now() - new_cutoff = now - timedelta(hours=new_hours) - old_cutoff = now - timedelta(days=old_days) + current_time = now() + new_cutoff = current_time - timedelta(hours=new_hours) + old_cutoff = current_time - timedelta(days=old_days) if len(signoff_groups) == 0: # no need to send an email at all diff --git a/packages/views/flag.py b/packages/views/flag.py index 7fa2d508..f3db93b3 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -5,12 +5,12 @@ from django.db import transaction from django.shortcuts import get_object_or_404, redirect from django.template import loader, Context +from django.utils.timezone import now from django.views.generic.simple import direct_to_template from django.views.decorators.cache import cache_page, never_cache from ..models import FlagRequest from main.models import Package -from main.utils import utc_now class FlagForm(forms.Form): @@ -76,10 +76,10 @@ def flag(request, name, repo, arch): @transaction.commit_on_success def perform_updates(): - now = utc_now() - pkgs.update(flag_date=now) + current_time = now() + pkgs.update(flag_date=current_time) # store our flag request - flag_request = FlagRequest(created=now, + flag_request = FlagRequest(created=current_time, user_email=email, message=message, ip_address=ip_addr, pkgbase=pkg.pkgbase, version=version, repo=pkg.repo, diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 61d949fc..7aa39106 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -8,11 +8,11 @@ from django.db import transaction from django.http import HttpResponse, Http404 from django.shortcuts import get_list_or_404, redirect, render +from django.utils.timezone import now from django.views.decorators.cache import never_cache from django.views.generic.simple import direct_to_template from main.models import Package, Arch, Repo -from main.utils import utc_now from ..models import SignoffSpecification, Signoff from ..utils import (get_signoff_groups, approved_by_signoffs, PackageSignoffGroup) @@ -45,7 +45,7 @@ def signoff_package(request, name, repo, arch, revoke=False): package, request.user, False) except Signoff.DoesNotExist: raise Http404 - signoff.revoked = utc_now() + signoff.revoked = now() signoff.save() created = False else: diff --git a/releng/management/commands/syncisos.py b/releng/management/commands/syncisos.py index 62f005ff..223c771b 100644 --- a/releng/management/commands/syncisos.py +++ b/releng/management/commands/syncisos.py @@ -4,8 +4,8 @@ from django.conf import settings from django.core.management.base import BaseCommand, CommandError +from django.utils.timezone import now -from main.utils import utc_now from releng.models import Iso @@ -54,9 +54,8 @@ def handle(self, *args, **options): existing.active = True existing.removed = None existing.save() - now = utc_now() # and then mark all other names as no longer active Iso.objects.filter(active=True).exclude(name__in=active_isos).update( - active=False, removed=now) + active=False, removed=now()) # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 76c37ce3acc7a4af0271c7535d4a33042f7749b5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 24 Jul 2012 09:35:55 -0500 Subject: Replace deprecated direct_to_template() with render() shortcut Now that Django actually provides a concise way to use a RequestContext object without instantiating it, we can use that rather than the old function-based generic view that worked well to do the same. Additionally, these function-based generic views will be gone in Django 1.5, so might as well make the move now. Signed-off-by: Dan McGee --- devel/views.py | 15 +++++++-------- mirrors/views.py | 19 +++++++++---------- packages/views/__init__.py | 11 +++++------ packages/views/display.py | 13 +++++-------- packages/views/flag.py | 12 +++++------- packages/views/signoff.py | 5 ++--- public/views.py | 14 +++++++------- releng/views.py | 15 +++++++-------- retro/views.py | 4 ++-- todolists/views.py | 13 ++++++------- visualize/views.py | 4 ++-- 11 files changed, 57 insertions(+), 68 deletions(-) diff --git a/devel/views.py b/devel/views.py index 143b12bf..f877bc84 100644 --- a/devel/views.py +++ b/devel/views.py @@ -17,11 +17,10 @@ from django.db import transaction from django.db.models import F, Count, Max from django.http import Http404 -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, render 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 django.utils.encoding import force_unicode from django.utils.http import http_date from django.utils.timezone import now @@ -88,7 +87,7 @@ def index(request): 'signoffs': signoffs } - return direct_to_template(request, 'devel/index.html', page_dict) + return render(request, 'devel/index.html', page_dict) @login_required def clock(request): @@ -128,7 +127,7 @@ def clock(request): 'utc_now': current_time, } - response = direct_to_template(request, 'devel/clock.html', page_dict) + response = render(request, 'devel/clock.html', page_dict) if not response.has_header('Expires'): expire_time = current_time.replace(second=0, microsecond=0) expire_time += timedelta(minutes=1) @@ -178,7 +177,7 @@ def change_profile(request): else: form = ProfileForm(initial={'email': request.user.email}) profile_form = UserProfileForm(instance=request.user.get_profile()) - return direct_to_template(request, 'devel/profile.html', + return render(request, 'devel/profile.html', {'form': form, 'profile_form': profile_form}) @login_required @@ -301,7 +300,7 @@ def report(request, report_name, username=None): 'column_names': names, 'column_attrs': attrs, } - return direct_to_template(request, 'devel/packages.html', context) + return render(request, 'devel/packages.html', context) class NewUserForm(forms.ModelForm): @@ -399,7 +398,7 @@ def inner_save(): 'title': 'Create User', 'submit_text': 'Create User' } - return direct_to_template(request, 'general_form.html', context) + return render(request, 'general_form.html', context) @user_passes_test(lambda u: u.is_superuser) def admin_log(request, username=None): @@ -410,6 +409,6 @@ def admin_log(request, username=None): 'title': "Admin Action Log", 'log_user': user, } - return direct_to_template(request, 'devel/admin_log.html', context) + return render(request, 'devel/admin_log.html', context) # vim: set ts=4 sw=4 et: diff --git a/mirrors/views.py b/mirrors/views.py index 8f092be7..400c084d 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -8,9 +8,8 @@ from django.core.serializers.json import DjangoJSONEncoder from django.db.models import Q from django.http import Http404, HttpResponse -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, render from django.views.decorators.csrf import csrf_exempt -from django.views.generic.simple import direct_to_template from django_countries.countries import COUNTRIES from .models import Mirror, MirrorUrl, MirrorProtocol, TIER_CHOICES @@ -76,7 +75,7 @@ def generate_mirrorlist(request): else: form = MirrorlistForm() - return direct_to_template(request, 'mirrors/mirrorlist_generate.html', + return render(request, 'mirrors/mirrorlist_generate.html', {'mirrorlist_form': form}) @@ -142,10 +141,10 @@ def find_mirrors(request, countries=None, protocols=None, use_status=False, urls = status_filter(urls) template = 'mirrors/mirrorlist_status.txt' - return direct_to_template(request, template, { - 'mirror_urls': urls, - }, - mimetype='text/plain') + context = { + 'mirror_urls': urls, + } + return render(request, template, context, content_type='text/plain') def find_mirrors_simple(request, protocol): @@ -161,7 +160,7 @@ def mirrors(request): mirror_list = Mirror.objects.select_related().order_by('tier', 'country') if not request.user.is_authenticated(): mirror_list = mirror_list.filter(public=True, active=True) - return direct_to_template(request, 'mirrors/mirrors.html', + return render(request, 'mirrors/mirrors.html', {'mirror_list': mirror_list}) @@ -180,7 +179,7 @@ def mirror_details(request, name): all_urls = set(checked_urls).union(all_urls) all_urls = sorted(all_urls, key=attrgetter('url')) - return direct_to_template(request, 'mirrors/mirror_details.html', + return render(request, 'mirrors/mirror_details.html', {'mirror': mirror, 'urls': all_urls}) @@ -217,7 +216,7 @@ def status(request, tier=None): 'error_logs': error_logs, 'tier': tier, }) - return direct_to_template(request, 'mirrors/status.html', context) + return render(request, 'mirrors/status.html', context) class MirrorStatusJSONEncoder(DjangoJSONEncoder): diff --git a/packages/views/__init__.py b/packages/views/__init__.py index fa67daa8..19ea9103 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -6,10 +6,9 @@ from django.contrib.auth.models import User from django.core.cache import cache from django.http import HttpResponse -from django.shortcuts import redirect +from django.shortcuts import redirect, render from django.views.decorators.cache import cache_control from django.views.decorators.http import require_GET, require_POST -from django.views.generic.simple import direct_to_template from main.models import Package, Arch from ..models import PackageRelation @@ -32,9 +31,9 @@ def opensearch(request): else: domain = "http://%s" % request.META['HTTP_HOST'] - return direct_to_template(request, 'packages/opensearch.xml', + return render(request, 'packages/opensearch.xml', {'domain': domain}, - mimetype='application/opensearchdescription+xml') + content_type='application/opensearchdescription+xml') @require_GET @@ -115,7 +114,7 @@ def arch_differences(request): 'differences': differences, 'multilib_differences': multilib_diffs } - return direct_to_template(request, 'packages/differences.html', context) + return render(request, 'packages/differences.html', context) @permission_required('main.change_package') def stale_relations(request): @@ -132,7 +131,7 @@ def stale_relations(request): 'missing_pkgbase': missing_pkgbase, 'wrong_permissions': wrong_permissions, } - return direct_to_template(request, 'packages/stale_relations.html', context) + return render(request, 'packages/stale_relations.html', context) @permission_required('packages.delete_packagerelation') @require_POST diff --git a/packages/views/display.py b/packages/views/display.py index 31f18c79..585e0e4e 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -6,7 +6,6 @@ from django.http import HttpResponse, Http404 from django.shortcuts import get_object_or_404, redirect, render from django.utils.timezone import now -from django.views.generic.simple import direct_to_template from main.models import Package, PackageFile, Arch, Repo from mirrors.utils import get_mirror_url_for_download @@ -33,8 +32,7 @@ def split_package_details(request, name, repo, arch): 'arch': arch, 'packages': pkgs, } - return direct_to_template(request, 'packages/packages_list.html', - context) + return render(request, 'packages/packages_list.html', context) CUTOFF = datetime.timedelta(days=60) @@ -67,8 +65,7 @@ def details(request, name='', repo='', arch=''): pkg = Package.objects.select_related( 'arch', 'repo', 'packager').get(pkgname=name, repo__name__iexact=repo, arch__name=arch) - return direct_to_template(request, 'packages/details.html', - {'pkg': pkg, }) + return render(request, 'packages/details.html', {'pkg': pkg}) except Package.DoesNotExist: arch_obj = get_object_or_404(Arch, name=arch) # for arch='any' packages, we can issue a redirect to them if we @@ -111,7 +108,7 @@ def groups(request, arch=None): 'groups': grps, 'arch': arch, } - return direct_to_template(request, 'packages/groups.html', context) + return render(request, 'packages/groups.html', context) def group_details(request, arch, name): @@ -128,7 +125,7 @@ def group_details(request, arch, name): 'arch': arch, 'packages': pkgs, } - return direct_to_template(request, 'packages/packages_list.html', context) + return render(request, 'packages/packages_list.html', context) def files(request, name, repo, arch): @@ -153,7 +150,7 @@ def files(request, name, repo, arch): 'dir_count': dir_count, } template = 'packages/files.html' - return direct_to_template(request, template, context) + return render(request, template, context) def details_json(request, name, repo, arch): diff --git a/packages/views/flag.py b/packages/views/flag.py index f3db93b3..b9542a62 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -3,10 +3,9 @@ from django.contrib.auth.decorators import permission_required from django.core.mail import send_mail from django.db import transaction -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import get_object_or_404, redirect, render from django.template import loader, Context from django.utils.timezone import now -from django.views.generic.simple import direct_to_template from django.views.decorators.cache import cache_page, never_cache from ..models import FlagRequest @@ -34,7 +33,7 @@ def __init__(self, *args, **kwargs): @cache_page(3600) def flaghelp(request): - return direct_to_template(request, 'packages/flaghelp.html') + return render(request, 'packages/flaghelp.html') @never_cache @@ -43,8 +42,7 @@ def flag(request, name, repo, arch): pkgname=name, repo__name__iexact=repo, arch__name=arch) if pkg.flag_date is not None: # already flagged. do nothing. - return direct_to_template(request, 'packages/flagged.html', - {'pkg': pkg}) + return render(request, 'packages/flagged.html', {'pkg': pkg}) # find all packages from (hopefully) the same PKGBUILD pkgs = Package.objects.normal().filter( pkgbase=pkg.pkgbase, flag_date__isnull=True, @@ -129,7 +127,7 @@ def perform_updates(): 'packages': pkgs, 'form': form } - return direct_to_template(request, 'packages/flag.html', context) + return render(request, 'packages/flag.html', context) def flag_confirmed(request, name, repo, arch): pkg = get_object_or_404(Package, @@ -142,7 +140,7 @@ def flag_confirmed(request, name, repo, arch): context = {'package': pkg, 'packages': pkgs} - return direct_to_template(request, 'packages/flag_confirmed.html', context) + return render(request, 'packages/flag_confirmed.html', context) @permission_required('main.change_package') def unflag(request, name, repo, arch): diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 7aa39106..56eb060c 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -10,7 +10,6 @@ from django.shortcuts import get_list_or_404, redirect, render from django.utils.timezone import now from django.views.decorators.cache import never_cache -from django.views.generic.simple import direct_to_template from main.models import Package, Arch, Repo from ..models import SignoffSpecification, Signoff @@ -28,7 +27,7 @@ def signoffs(request): 'arches': Arch.objects.all(), 'repo_names': sorted(set(g.target_repo for g in signoff_groups)), } - return direct_to_template(request, 'packages/signoffs.html', context) + return render(request, 'packages/signoffs.html', context) @permission_required('main.change_package') @never_cache @@ -144,7 +143,7 @@ def signoff_options(request, name, repo, arch): 'package': package, 'form': form, } - return direct_to_template(request, 'packages/signoff_options.html', context) + return render(request, 'packages/signoff_options.html', context) class SignoffJSONEncoder(DjangoJSONEncoder): '''Base JSONEncoder extended to handle all serialization of all classes diff --git a/public/views.py b/public/views.py index 3ea8f841..3f68545c 100644 --- a/public/views.py +++ b/public/views.py @@ -5,8 +5,8 @@ from django.contrib.auth.models import User from django.db.models import Count, Q from django.http import Http404 +from django.shortcuts import render from django.views.decorators.cache import cache_control -from django.views.generic.simple import direct_to_template from devel.models import MasterKey, PGPSignature from main.models import Arch, Repo, Donor @@ -21,7 +21,7 @@ def index(request): 'news_updates': News.objects.order_by('-postdate', '-id')[:15], 'pkg_updates': pkgs, } - return direct_to_template(request, 'public/index.html', context) + return render(request, 'public/index.html', context) USER_LISTS = { 'devs': { @@ -55,14 +55,14 @@ def userlist(request, user_type='devs'): users = users.distinct() context = USER_LISTS[user_type].copy() context['users'] = users - return direct_to_template(request, 'public/userlist.html', context) + return render(request, 'public/userlist.html', context) @cache_control(max_age=300) def donate(request): context = { 'donors': Donor.objects.filter(visible=True).order_by('name'), } - return direct_to_template(request, 'public/donate.html', context) + return render(request, 'public/donate.html', context) @cache_control(max_age=300) def download(request): @@ -76,7 +76,7 @@ def download(request): 'releng_pxeboot_url': settings.PXEBOOT_URL, 'mirror_urls': mirror_urls, } - return direct_to_template(request, 'public/download.html', context) + return render(request, 'public/download.html', context) @cache_control(max_age=300) def feeds(request): @@ -84,7 +84,7 @@ def feeds(request): 'arches': Arch.objects.all(), 'repos': Repo.objects.all(), } - return direct_to_template(request, 'public/feeds.html', context) + return render(request, 'public/feeds.html', context) @cache_control(max_age=300) def keys(request): @@ -113,6 +113,6 @@ def keys(request): 'active_users': users, 'signatures': signatures, } - return direct_to_template(request, 'public/keys.html', context) + return render(request, 'public/keys.html', context) # vim: set ts=4 sw=4 et: diff --git a/releng/views.py b/releng/views.py index fc81d410..a1bc3b81 100644 --- a/releng/views.py +++ b/releng/views.py @@ -2,8 +2,7 @@ from django.conf import settings from django.db.models import Count, Max from django.http import Http404 -from django.shortcuts import get_object_or_404, redirect -from django.views.generic.simple import direct_to_template +from django.shortcuts import get_object_or_404, redirect, render from .models import (Architecture, BootType, Bootloader, ClockChoice, Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source, @@ -73,7 +72,7 @@ def submit_test_result(request): form = TestForm() context = {'form': form} - return direct_to_template(request, 'releng/add.html', context) + return render(request, 'releng/add.html', context) def calculate_option_overview(field_name): field = Test._meta.get_field(field_name) @@ -145,7 +144,7 @@ def test_results_overview(request): 'options': all_options, 'iso_url': settings.ISO_LIST_URL, } - return direct_to_template(request, 'releng/results.html', context) + return render(request, 'releng/results.html', context) def test_results_iso(request, iso_id): iso = get_object_or_404(Iso, pk=iso_id) @@ -154,7 +153,7 @@ def test_results_iso(request, iso_id): 'iso_name': iso.name, 'test_list': test_list } - return direct_to_template(request, 'releng/result_list.html', context) + return render(request, 'releng/result_list.html', context) def test_results_for(request, option, value): if option not in Test._meta.get_all_field_names(): @@ -170,10 +169,10 @@ def test_results_for(request, option, value): 'value_id': value, 'test_list': test_list } - return direct_to_template(request, 'releng/result_list.html', context) + return render(request, 'releng/result_list.html', context) def submit_test_thanks(request): - return direct_to_template(request, "releng/thanks.html", None) + return render(request, "releng/thanks.html", None) def iso_overview(request): isos = Iso.objects.all().order_by('-pk') @@ -192,6 +191,6 @@ def iso_overview(request): context = { 'isos': isos } - return direct_to_template(request, 'releng/iso_overview.html', context) + return render(request, 'releng/iso_overview.html', context) # vim: set ts=4 sw=4 et: diff --git a/retro/views.py b/retro/views.py index 3bc59e9f..31226deb 100644 --- a/retro/views.py +++ b/retro/views.py @@ -1,6 +1,6 @@ from django.http import Http404 +from django.shortcuts import render from django.views.decorators.cache import cache_page -from django.views.generic.simple import direct_to_template RETRO_YEAR_MAP = { @@ -26,6 +26,6 @@ def retro_homepage(request, year): context = { 'year': year, } - return direct_to_template(request, 'retro/%s' % template, context) + return render(request, 'retro/%s' % template, context) # vim: set ts=4 sw=4 et: diff --git a/todolists/views.py b/todolists/views.py index 580ec00f..c7ba2560 100644 --- a/todolists/views.py +++ b/todolists/views.py @@ -3,12 +3,11 @@ from django import forms from django.http import HttpResponse from django.core.mail import send_mail -from django.shortcuts import get_list_or_404, get_object_or_404, redirect +from django.shortcuts import get_list_or_404, get_object_or_404, redirect, render from django.contrib.auth.decorators import login_required, permission_required from django.db import transaction from django.views.decorators.cache import never_cache from django.views.generic import DeleteView -from django.views.generic.simple import direct_to_template from django.template import Context, loader from main.models import Todolist, TodolistPkg, Package, Repo @@ -54,7 +53,7 @@ def view(request, list_id): # we don't hold onto the result, but the objects are the same here, # so accessing maintainers in the template is now cheap attach_maintainers(tp.pkg for tp in todolist.packages) - return direct_to_template(request, 'todolists/view.html', { + return render(request, 'todolists/view.html', { 'list': todolist, 'svn_roots': svn_roots, }) @@ -72,7 +71,7 @@ def list_pkgbases(request, list_id, svn_root): @login_required def todolist_list(request): lists = get_annotated_todolists() - return direct_to_template(request, 'todolists/list.html', {'lists': lists}) + return render(request, 'todolists/list.html', {'lists': lists}) @permission_required('main.add_todolist') @never_cache @@ -92,7 +91,7 @@ def add(request): 'form': form, 'submit_text': 'Create List' } - return direct_to_template(request, 'general_form.html', page_dict) + return render(request, 'general_form.html', page_dict) # TODO: this calls for transaction management and async emailing @permission_required('main.change_todolist') @@ -115,7 +114,7 @@ def edit(request, list_id): 'form': form, 'submit_text': 'Save List' } - return direct_to_template(request, 'general_form.html', page_dict) + return render(request, 'general_form.html', page_dict) class DeleteTodolist(DeleteView): model = Todolist @@ -185,7 +184,7 @@ def public_list(request): # total hackjob, but it makes this a lot less query-intensive. all_pkgs = [tp for tl in todo_lists for tp in tl.packages] attach_maintainers([tp.pkg for tp in all_pkgs]) - return direct_to_template(request, "todolists/public_list.html", + return render(request, "todolists/public_list.html", {"todo_lists": todo_lists}) # vim: set ts=4 sw=4 et: diff --git a/visualize/views.py b/visualize/views.py index 95f8c314..44e60472 100644 --- a/visualize/views.py +++ b/visualize/views.py @@ -4,14 +4,14 @@ from django.contrib.auth.models import User from django.db.models import Count, Sum, Q from django.http import HttpResponse +from django.shortcuts import render from django.views.decorators.cache import cache_page -from django.views.generic.simple import direct_to_template from main.models import Package, Arch, Repo from devel.models import MasterKey, PGPSignature def index(request): - return direct_to_template(request, 'visualize/index.html', {}) + return render(request, 'visualize/index.html') def arch_repo_data(): qs = Package.objects.select_related().values( -- cgit v1.2.3-54-g00ecf From 590060d1b4f7e82a0c637a5e8dc5d348431651af Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 24 Jul 2012 09:43:20 -0500 Subject: Remove no-longer needed handler imports This was finally fixed upstream in https://code.djangoproject.com/ticket/5350. Signed-off-by: Dan McGee --- urls.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/urls.py b/urls.py index 75ef7e52..65f436d3 100644 --- a/urls.py +++ b/urls.py @@ -1,7 +1,6 @@ import os.path -# Stupid Django. Don't remove these "unused" handler imports -from django.conf.urls import handler500, handler404, include, patterns +from django.conf.urls import include, patterns from django.conf import settings from django.contrib import admin -- cgit v1.2.3-54-g00ecf From d68e00f19556be169f303da256c79431e15dd448 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 24 Jul 2012 19:56:22 -0500 Subject: Remove use of deprecated redirect_to function-based generic view We can use the class-based replacement instead. Signed-off-by: Dan McGee --- urls.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/urls.py b/urls.py index 65f436d3..d777b866 100644 --- a/urls.py +++ b/urls.py @@ -1,11 +1,8 @@ -import os.path - -from django.conf.urls import include, patterns -from django.conf import settings +from django.conf.urls import include, patterns, url from django.contrib import admin -from django.views.generic import TemplateView from django.views.decorators.cache import cache_page +from django.views.generic import TemplateView, RedirectView from django.views.i18n import null_javascript_catalog from feeds import PackageFeed, NewsFeed @@ -105,8 +102,7 @@ ('^packages.php', '/packages/'), ) -for old_url, new_url in legacy_urls: - urlpatterns += patterns('django.views.generic.simple', - (old_url, 'redirect_to', {'url': new_url})) +urlpatterns += [url(old_url, RedirectView.as_view(url=new_url)) + for old_url, new_url in legacy_urls] # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 24b28a504cabcf077882aa95cfa0edbc6a8d4569 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 24 Jul 2012 21:01:31 -0500 Subject: Replace deprecated list_detail usage in search with class-based view We can convert the entire search view to a generic class-based ListView. This is still one of the more disgusting views in the application and has a ton of logic scattered buckshot across several methods, but this commit is not meant to address all of that in one go. This is the last of the deprecated pieces I know of we are still using in the codebase, so we should be relatively safe in the long run now for an upgrade to the eventual next major Django release. Signed-off-by: Dan McGee --- packages/urls.py | 8 +++-- packages/views/__init__.py | 2 +- packages/views/search.py | 77 +++++++++++++++++++++++----------------------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/packages/urls.py b/packages/urls.py index 6eddc5fe..9a151b4b 100644 --- a/packages/urls.py +++ b/packages/urls.py @@ -1,5 +1,7 @@ from django.conf.urls import include, patterns +from .views.search import SearchListView + package_patterns = patterns('packages.views', (r'^$', 'details'), (r'^json/$', 'details_json'), @@ -21,9 +23,9 @@ (r'^signoffs/json/$', 'signoffs_json', {}, 'package-signoffs-json'), (r'^update/$', 'update'), - (r'^$', 'search', {}, 'packages-search'), - (r'^search/json/$', 'search_json'), - (r'^(?P\d+)/$', 'search'), + (r'^$', SearchListView.as_view(), {}, 'packages-search'), + (r'^(?P\d+)/$', SearchListView.as_view()), + (r'^search/json/$', 'search_json'), (r'^differences/$', 'arch_differences', {}, 'packages-differences'), (r'^stale_relations/$', 'stale_relations'), diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 19ea9103..038d40ac 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -19,7 +19,7 @@ from .display import (details, groups, group_details, files, details_json, files_json, download) from .flag import flaghelp, flag, flag_confirmed, unflag, unflag_all -from .search import search, search_json +from .search import search_json from .signoff import signoffs, signoff_package, signoff_options, signoffs_json diff --git a/packages/views/search.py b/packages/views/search.py index a89822be..9750894a 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import User from django.db.models import Q from django.http import HttpResponse -from django.views.generic import list_detail +from django.views.generic import ListView from main.models import Package, Arch, Repo from main.utils import make_choice @@ -33,6 +33,7 @@ def valid_value(self, value): except (ValueError, TypeError): return False + class PackageSearchForm(forms.Form): repo = forms.MultipleChoiceField(required=False) arch = forms.MultipleChoiceField(required=False) @@ -69,6 +70,7 @@ def __init__(self, *args, **kwargs): [('', 'All'), ('unknown', 'Unknown')] + \ [(m.username, m.get_full_name()) for m in maints] + def parse_form(form, packages): if form.cleaned_data['repo']: packages = packages.filter( @@ -117,48 +119,47 @@ def parse_form(form, packages): return packages -def search(request, page=None): - limit = 50 - sort = None - packages = Package.objects.normal() - if request.GET: - form = PackageSearchForm(data=request.GET) - if form.is_valid(): - packages = parse_form(form, packages) - asked_limit = form.cleaned_data['limit'] +class SearchListView(ListView): + template_name = "packages/search.html" + + sort_fields = ("arch", "repo", "pkgname", "pkgbase", "compressed_size", + "installed_size", "build_date", "last_update", "flag_date") + allowed_sort = list(sort_fields) + ["-" + s for s in sort_fields] + + def get(self, request, *args, **kwargs): + self.form = PackageSearchForm(data=request.GET) + return super(SearchListView, self).get(request, *args, **kwargs) + + def get_queryset(self): + packages = Package.objects.normal() + if self.form.is_valid(): + packages = parse_form(self.form, packages) + sort = self.form.cleaned_data['sort'] + if sort in self.allowed_sort: + packages = packages.order_by(sort) + else: + packages = packages.order_by('pkgname') + return packages + + # Form had errors so don't return any results + return Package.objects.none() + + def get_paginate_by(self, queryset): + limit = 50 + if self.form.is_valid(): + asked_limit = self.form.cleaned_data['limit'] if asked_limit and asked_limit < 0: limit = None elif asked_limit: limit = asked_limit - sort = form.cleaned_data['sort'] - else: - # Form had errors, don't return any results, just the busted form - packages = Package.objects.none() - else: - form = PackageSearchForm() - - current_query = request.GET.urlencode() - page_dict = { - 'search_form': form, - 'current_query': current_query - } - allowed_sort = ["arch", "repo", "pkgname", "pkgbase", - "compressed_size", "installed_size", - "build_date", "last_update", "flag_date"] - allowed_sort += ["-" + s for s in allowed_sort] - if sort in allowed_sort: - packages = packages.order_by(sort) - page_dict['sort'] = sort - else: - packages = packages.order_by('pkgname') - - return list_detail.object_list(request, packages, - template_name="packages/search.html", - page=page, - paginate_by=limit, - template_object_name="package", - extra_context=page_dict) + return limit + + def get_context_data(self, **kwargs): + context = super(SearchListView, self).get_context_data(**kwargs) + context['current_query'] = self.request.GET.urlencode() + context['search_form'] = self.form + return context def search_json(request): -- cgit v1.2.3-54-g00ecf From c1a6a87e23864ea044cb15f76b9dbb16734f08d8 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 25 Jul 2012 00:45:36 -0500 Subject: Add arch and repo filter to todolist packages This matches what we do on signoffs. Also beef up the styling a bit and add the dynamically updated package count info. Signed-off-by: Dan McGee --- sitestatic/archweb.js | 9 +++++++++ templates/todolists/view.html | 16 ++++++++++++++-- todolists/views.py | 4 ++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/sitestatic/archweb.js b/sitestatic/archweb.js index e6d73414..6586d312 100644 --- a/sitestatic/archweb.js +++ b/sitestatic/archweb.js @@ -287,12 +287,21 @@ function filter_todolist() { if ($('#id_mine_only').is(':checked')) { rows = rows.filter('.mine'); } + /* apply arch and repo filters */ + $('#todolist_filter .arch_filter').add( + '#todolist_filter .repo_filter').each(function() { + if (!$(this).is(':checked')) { + rows = rows.not('.' + $(this).val()); + } + }); + /* more expensive filter because of 'has' call */ if ($('#id_incomplete').is(':checked')) { rows = rows.has('.incomplete'); } /* hide all rows, then show the set we care about */ all_rows.hide(); rows.show(); + $('#filter-count').text(rows.length); /* make sure we update the odd/even styling from sorting */ $('.results').trigger('applyWidgets'); } diff --git a/templates/todolists/view.html b/templates/todolists/view.html index 69595e1a..35bc9446 100644 --- a/templates/todolists/view.html +++ b/templates/todolists/view.html @@ -29,16 +29,28 @@

    Todo List: {{ list.name }}

  • {{ svn_root }}
  • {% endfor %} -
    +

    {{ list.packages|length }} total todolist package{{ list.packages|pluralize }} found.

    + +

    Filter Todolist Packages

    Select filter criteria + {% for arch in arches %} +
    +
    + {% endfor %} + {% for repo in repos %} +
    +
    + {% endfor %}
    +
    +
    {{ list.packages|length }} todolist packages displayed.
    @@ -56,7 +68,7 @@

    Filter Todolist Packages

    {% for pkg in list.packages %} - + {{ pkg.pkg.arch.name }} {{ pkg.pkg.repo.name|capfirst }} {% pkg_details_link pkg.pkg %} diff --git a/todolists/views.py b/todolists/views.py index c7ba2560..b8d1dae1 100644 --- a/todolists/views.py +++ b/todolists/views.py @@ -53,9 +53,13 @@ def view(request, list_id): # we don't hold onto the result, but the objects are the same here, # so accessing maintainers in the template is now cheap attach_maintainers(tp.pkg for tp in todolist.packages) + arches = set(tp.pkg.arch for tp in todolist.packages) + repos = set(tp.pkg.repo for tp in todolist.packages) return render(request, 'todolists/view.html', { 'list': todolist, 'svn_roots': svn_roots, + 'arches': sorted(arches), + 'repos': sorted(repos), }) # really no need for login_required on this one... -- cgit v1.2.3-54-g00ecf From b055a31c3450ea3e20d4726db2ca7657bc06597c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 25 Jul 2012 00:46:33 -0500 Subject: Bump Markdown version Signed-off-by: Dan McGee --- requirements.txt | 2 +- requirements_prod.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 146ccbcc..140cae99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ Django==1.4 -Markdown==2.1.1 +Markdown==2.2.0 South==0.7.5 django-countries==1.2 pgpdump==1.3 diff --git a/requirements_prod.txt b/requirements_prod.txt index 2f0dec50..d0f34582 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -1,5 +1,5 @@ Django==1.4 -Markdown==2.1.1 +Markdown==2.2.0 South==0.7.5 django-countries==1.2 pgpdump==1.3 -- cgit v1.2.3-54-g00ecf From 4c446e5cf71e6755f87ca08cf851569a2c13614b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 28 Jul 2012 11:45:08 -0500 Subject: Convert releng URLs to https by default Signed-off-by: Dan McGee --- settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.py b/settings.py index 50ed6c18..8b74e554 100644 --- a/settings.py +++ b/settings.py @@ -127,10 +127,10 @@ PGP_SERVER = 'pgp.mit.edu:11371' # URL to fetch a current list of available ISOs -ISO_LIST_URL = 'http://releng.archlinux.org/isos/' +ISO_LIST_URL = 'https://releng.archlinux.org/isos/' # URL to the PXE netboot instructions -PXEBOOT_URL = 'http://releng.archlinux.org/pxeboot/' +PXEBOOT_URL = 'https://releng.archlinux.org/pxeboot/' # URL for SVN access for fetching commit messages (note absence of packages or # community bit on the end, repo.svn_root is appended) -- cgit v1.2.3-54-g00ecf From 280c53eec5661252b5692fa374292c4d421e3bd8 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 28 Jul 2012 11:45:15 -0500 Subject: reporead: don't use iexact lookup on arch name We don't do this anywhere else, so we shouldn't do this here either. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index e69691db..aaa9812e 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -332,7 +332,7 @@ def update_common(archname, reponame, pkgs, sanity_check=True): transaction.set_dirty() repository = Repo.objects.get(name__iexact=reponame) - architecture = Arch.objects.get(name__iexact=archname) + architecture = Arch.objects.get(name=archname) # no-arg order_by() removes even the default ordering; we don't need it dbpkgs = Package.objects.filter( arch=architecture, repo=repository).order_by() @@ -371,7 +371,7 @@ def db_update(archname, reponame, pkgs, force=False): logger.info('Updating %s (%s)', reponame, archname) dbpkgs = update_common(archname, reponame, pkgs, True) repository = Repo.objects.get(name__iexact=reponame) - architecture = Arch.objects.get(name__iexact=archname) + architecture = Arch.objects.get(name=archname) # This makes our inner loop where we find packages by name *way* more # efficient by not having to go to the database for each package to @@ -538,7 +538,7 @@ def locate_arch(arch): if isinstance(arch, Arch): return arch try: - return Arch.objects.get(name__iexact=arch) + return Arch.objects.get(name=arch) except Arch.DoesNotExist: raise CommandError( 'Specified architecture %s is not currently known.' % arch) -- cgit v1.2.3-54-g00ecf From 2a221fa72fd8491ca1a0633ba62b6a846267b4fc Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 9 Jul 2012 00:40:17 -0500 Subject: Upgrade to jQuery 1.7.2 and a maintained tablesorter This touches a wide variety of files as well as makes updates to some of our own code to be fully compatible. We also use some of the newer locale/accent sorting features of tablesorter to make tables with developer names sort in a more sane fashion. Signed-off-by: Dan McGee --- main/templatetags/cdn.py | 5 +- sitestatic/archweb.css | 12 +- sitestatic/archweb.js | 19 +- sitestatic/jquery-1.4.4.min.js | 167 ---- sitestatic/jquery-1.7.2.min.js | 4 + sitestatic/jquery.tablesorter-2.3.11.js | 1181 +++++++++++++++++++++++++++ sitestatic/jquery.tablesorter-2.3.11.min.js | 6 + sitestatic/jquery.tablesorter.js | 1031 ----------------------- sitestatic/jquery.tablesorter.min.js | 4 - templates/devel/clock.html | 1 + templates/devel/index.html | 7 +- templates/public/keys.html | 1 + 12 files changed, 1217 insertions(+), 1221 deletions(-) delete mode 100644 sitestatic/jquery-1.4.4.min.js create mode 100644 sitestatic/jquery-1.7.2.min.js create mode 100644 sitestatic/jquery.tablesorter-2.3.11.js create mode 100644 sitestatic/jquery.tablesorter-2.3.11.min.js delete mode 100644 sitestatic/jquery.tablesorter.js delete mode 100644 sitestatic/jquery.tablesorter.min.js diff --git a/main/templatetags/cdn.py b/main/templatetags/cdn.py index 54299823..fb78039c 100644 --- a/main/templatetags/cdn.py +++ b/main/templatetags/cdn.py @@ -7,7 +7,7 @@ @register.simple_tag def jquery(): - version = '1.4.4' + version = '1.7.2' oncdn = getattr(settings, 'CDN_ENABLED', True) if oncdn: link = 'https://ajax.googleapis.com/ajax/libs/jquery/' \ @@ -20,7 +20,8 @@ def jquery(): @register.simple_tag def jquery_tablesorter(): - filename = 'jquery.tablesorter.min.js' + version = '2.3.11' + filename = 'jquery.tablesorter-%s.min.js' % version link = staticfiles_storage.url(filename) return '' % link diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 0638d711..be1dde1a 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -273,7 +273,7 @@ table.pretty2 { } /* additional styles for JS sorting */ - table.pretty2 th.header { + table.pretty2 th.tablesorter-header { padding-right: 20px; background-image: url(nosort.gif); background-repeat: no-repeat; @@ -281,11 +281,11 @@ table.pretty2 { cursor: pointer; } - table.pretty2 th.headerSortDown { + table.pretty2 th.tablesorter-headerSortDown { background-image: url(desc.gif); } - table.pretty2 th.headerSortUp { + table.pretty2 th.tablesorter-headerSortUp { background-image: url(asc.gif); } @@ -617,7 +617,7 @@ table.results { } /* additional styles for JS sorting */ - table.results th.header { + table.results th.tablesorter-header { padding-right: 20px; background-image: url(nosort.gif); background-repeat: no-repeat; @@ -625,12 +625,12 @@ table.results { cursor: pointer; } - table.results th.headerSortDown { + table.results th.tablesorter-headerSortDown { background-color: #e4eeff; background-image: url(desc.gif); } - table.results th.headerSortUp { + table.results th.tablesorter-headerSortUp { background-color: #e4eeff; background-image: url(asc.gif); } diff --git a/sitestatic/archweb.js b/sitestatic/archweb.js index 6586d312..9e9bddf6 100644 --- a/sitestatic/archweb.js +++ b/sitestatic/archweb.js @@ -26,11 +26,11 @@ if (typeof $.tablesorter !== 'undefined') { var c = table.config; return ($.inArray(s, this.special) > -1) || $.tablesorter.isDigit(s, c); }, - format: function(s) { + format: function(s, t) { if ($.inArray(s, this.special) > -1) { return Number.MAX_VALUE; } - return $.tablesorter.formatFloat(s); + return $.tablesorter.formatFloat(s, t); }, type: 'numeric' }); @@ -73,7 +73,7 @@ if (typeof $.tablesorter !== 'undefined') { is: function(s) { return this.re.test(s); }, - format: function(s) { + format: function(s, t) { var matches = this.re.exec(s); if (!matches) { return 0; @@ -86,7 +86,7 @@ if (typeof $.tablesorter !== 'undefined') { * between 0-11, because things have to be difficult. */ var date = new Date(matches[1], matches[2] - 1, matches[3], matches[4], matches[5], matches[7]); - return $.tablesorter.formatFloat(date.getTime()); + return $.tablesorter.formatFloat(date.getTime(), t); }, type: 'numeric' }); @@ -253,7 +253,7 @@ function filter_packages() { all_rows.hide(); rows.show(); /* make sure we update the odd/even styling from sorting */ - $('.results').trigger('applyWidgets'); + $('.results').trigger('applyWidgets', [false]); } function filter_packages_reset() { $('#id_archonly').val('both'); @@ -274,7 +274,7 @@ function todolist_flag() { 'incomplete').removeClass('complete'); } /* let tablesorter know the cell value has changed */ - $('.results').trigger('updateCell', $(link).closest('td')); + $('.results').trigger('updateCell', [$(link).closest('td')[0], false, null]); }); return false; } @@ -303,7 +303,7 @@ function filter_todolist() { rows.show(); $('#filter-count').text(rows.length); /* make sure we update the odd/even styling from sorting */ - $('.results').trigger('applyWidgets'); + $('.results').trigger('applyWidgets', [false]); } function filter_todolist_reset() { $('#id_incomplete').removeAttr('checked'); @@ -360,7 +360,8 @@ function signoff_package() { link.text('Revoke Signoff'); link.attr('href', base_href + '/signoff/revoke/'); } - $('.results').trigger('updateCell', approved); + /* let tablesorter know the cell value has changed */ + $('.results').trigger('updateCell', [approved[0], false, null]); }); return false; } @@ -385,7 +386,7 @@ function filter_signoffs() { rows.show(); $('#filter-count').text(rows.length); /* make sure we update the odd/even styling from sorting */ - $('.results').trigger('applyWidgets'); + $('.results').trigger('applyWidgets', [false]); } function filter_signoffs_reset() { $('#signoffs_filter .arch_filter').attr('checked', 'checked'); diff --git a/sitestatic/jquery-1.4.4.min.js b/sitestatic/jquery-1.4.4.min.js deleted file mode 100644 index 8f3ca2e2..00000000 --- a/sitestatic/jquery-1.4.4.min.js +++ /dev/null @@ -1,167 +0,0 @@ -/*! - * jQuery JavaScript Library v1.4.4 - * http://jquery.com/ - * - * Copyright 2010, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * Copyright 2010, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * - * Date: Thu Nov 11 19:04:53 2010 -0500 - */ -(function(E,B){function ka(a,b,d){if(d===B&&a.nodeType===1){d=a.getAttribute("data-"+b);if(typeof d==="string"){try{d=d==="true"?true:d==="false"?false:d==="null"?null:!c.isNaN(d)?parseFloat(d):Ja.test(d)?c.parseJSON(d):d}catch(e){}c.data(a,b,d)}else d=B}return d}function U(){return false}function ca(){return true}function la(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function Ka(a){var b,d,e,f,h,l,k,o,x,r,A,C=[];f=[];h=c.data(this,this.nodeType?"events":"__events__");if(typeof h==="function")h= -h.events;if(!(a.liveFired===this||!h||!h.live||a.button&&a.type==="click")){if(a.namespace)A=RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)");a.liveFired=this;var J=h.live.slice(0);for(k=0;kd)break;a.currentTarget=f.elem;a.data=f.handleObj.data;a.handleObj=f.handleObj;A=f.handleObj.origHandler.apply(f.elem,arguments);if(A===false||a.isPropagationStopped()){d=f.level;if(A===false)b=false;if(a.isImmediatePropagationStopped())break}}return b}}function Y(a,b){return(a&&a!=="*"?a+".":"")+b.replace(La, -"`").replace(Ma,"&")}function ma(a,b,d){if(c.isFunction(b))return c.grep(a,function(f,h){return!!b.call(f,h,f)===d});else if(b.nodeType)return c.grep(a,function(f){return f===b===d});else if(typeof b==="string"){var e=c.grep(a,function(f){return f.nodeType===1});if(Na.test(b))return c.filter(b,e,!d);else b=c.filter(b,e)}return c.grep(a,function(f){return c.inArray(f,b)>=0===d})}function na(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var e=c.data(a[d++]),f=c.data(this, -e);if(e=e&&e.events){delete f.handle;f.events={};for(var h in e)for(var l in e[h])c.event.add(this,h,e[h][l],e[h][l].data)}}})}function Oa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function oa(a,b,d){var e=b==="width"?a.offsetWidth:a.offsetHeight;if(d==="border")return e;c.each(b==="width"?Pa:Qa,function(){d||(e-=parseFloat(c.css(a,"padding"+this))||0);if(d==="margin")e+=parseFloat(c.css(a, -"margin"+this))||0;else e-=parseFloat(c.css(a,"border"+this+"Width"))||0});return e}function da(a,b,d,e){if(c.isArray(b)&&b.length)c.each(b,function(f,h){d||Ra.test(a)?e(a,h):da(a+"["+(typeof h==="object"||c.isArray(h)?f:"")+"]",h,d,e)});else if(!d&&b!=null&&typeof b==="object")c.isEmptyObject(b)?e(a,""):c.each(b,function(f,h){da(a+"["+f+"]",h,d,e)});else e(a,b)}function S(a,b){var d={};c.each(pa.concat.apply([],pa.slice(0,b)),function(){d[this]=a});return d}function qa(a){if(!ea[a]){var b=c("<"+ -a+">").appendTo("body"),d=b.css("display");b.remove();if(d==="none"||d==="")d="block";ea[a]=d}return ea[a]}function fa(a){return c.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var t=E.document,c=function(){function a(){if(!b.isReady){try{t.documentElement.doScroll("left")}catch(j){setTimeout(a,1);return}b.ready()}}var b=function(j,s){return new b.fn.init(j,s)},d=E.jQuery,e=E.$,f,h=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,l=/\S/,k=/^\s+/,o=/\s+$/,x=/\W/,r=/\d/,A=/^<(\w+)\s*\/?>(?:<\/\1>)?$/, -C=/^[\],:{}\s]*$/,J=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,w=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,I=/(?:^|:|,)(?:\s*\[)+/g,L=/(webkit)[ \/]([\w.]+)/,g=/(opera)(?:.*version)?[ \/]([\w.]+)/,i=/(msie) ([\w.]+)/,n=/(mozilla)(?:.*? rv:([\w.]+))?/,m=navigator.userAgent,p=false,q=[],u,y=Object.prototype.toString,F=Object.prototype.hasOwnProperty,M=Array.prototype.push,N=Array.prototype.slice,O=String.prototype.trim,D=Array.prototype.indexOf,R={};b.fn=b.prototype={init:function(j, -s){var v,z,H;if(!j)return this;if(j.nodeType){this.context=this[0]=j;this.length=1;return this}if(j==="body"&&!s&&t.body){this.context=t;this[0]=t.body;this.selector="body";this.length=1;return this}if(typeof j==="string")if((v=h.exec(j))&&(v[1]||!s))if(v[1]){H=s?s.ownerDocument||s:t;if(z=A.exec(j))if(b.isPlainObject(s)){j=[t.createElement(z[1])];b.fn.attr.call(j,s,true)}else j=[H.createElement(z[1])];else{z=b.buildFragment([v[1]],[H]);j=(z.cacheable?z.fragment.cloneNode(true):z.fragment).childNodes}return b.merge(this, -j)}else{if((z=t.getElementById(v[2]))&&z.parentNode){if(z.id!==v[2])return f.find(j);this.length=1;this[0]=z}this.context=t;this.selector=j;return this}else if(!s&&!x.test(j)){this.selector=j;this.context=t;j=t.getElementsByTagName(j);return b.merge(this,j)}else return!s||s.jquery?(s||f).find(j):b(s).find(j);else if(b.isFunction(j))return f.ready(j);if(j.selector!==B){this.selector=j.selector;this.context=j.context}return b.makeArray(j,this)},selector:"",jquery:"1.4.4",length:0,size:function(){return this.length}, -toArray:function(){return N.call(this,0)},get:function(j){return j==null?this.toArray():j<0?this.slice(j)[0]:this[j]},pushStack:function(j,s,v){var z=b();b.isArray(j)?M.apply(z,j):b.merge(z,j);z.prevObject=this;z.context=this.context;if(s==="find")z.selector=this.selector+(this.selector?" ":"")+v;else if(s)z.selector=this.selector+"."+s+"("+v+")";return z},each:function(j,s){return b.each(this,j,s)},ready:function(j){b.bindReady();if(b.isReady)j.call(t,b);else q&&q.push(j);return this},eq:function(j){return j=== --1?this.slice(j):this.slice(j,+j+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(N.apply(this,arguments),"slice",N.call(arguments).join(","))},map:function(j){return this.pushStack(b.map(this,function(s,v){return j.call(s,v,s)}))},end:function(){return this.prevObject||b(null)},push:M,sort:[].sort,splice:[].splice};b.fn.init.prototype=b.fn;b.extend=b.fn.extend=function(){var j,s,v,z,H,G=arguments[0]||{},K=1,Q=arguments.length,ga=false; -if(typeof G==="boolean"){ga=G;G=arguments[1]||{};K=2}if(typeof G!=="object"&&!b.isFunction(G))G={};if(Q===K){G=this;--K}for(;K0))if(q){var s=0,v=q;for(q=null;j=v[s++];)j.call(t,b);b.fn.trigger&&b(t).trigger("ready").unbind("ready")}}},bindReady:function(){if(!p){p=true;if(t.readyState==="complete")return setTimeout(b.ready,1);if(t.addEventListener){t.addEventListener("DOMContentLoaded",u,false);E.addEventListener("load",b.ready,false)}else if(t.attachEvent){t.attachEvent("onreadystatechange",u);E.attachEvent("onload", -b.ready);var j=false;try{j=E.frameElement==null}catch(s){}t.documentElement.doScroll&&j&&a()}}},isFunction:function(j){return b.type(j)==="function"},isArray:Array.isArray||function(j){return b.type(j)==="array"},isWindow:function(j){return j&&typeof j==="object"&&"setInterval"in j},isNaN:function(j){return j==null||!r.test(j)||isNaN(j)},type:function(j){return j==null?String(j):R[y.call(j)]||"object"},isPlainObject:function(j){if(!j||b.type(j)!=="object"||j.nodeType||b.isWindow(j))return false;if(j.constructor&& -!F.call(j,"constructor")&&!F.call(j.constructor.prototype,"isPrototypeOf"))return false;for(var s in j);return s===B||F.call(j,s)},isEmptyObject:function(j){for(var s in j)return false;return true},error:function(j){throw j;},parseJSON:function(j){if(typeof j!=="string"||!j)return null;j=b.trim(j);if(C.test(j.replace(J,"@").replace(w,"]").replace(I,"")))return E.JSON&&E.JSON.parse?E.JSON.parse(j):(new Function("return "+j))();else b.error("Invalid JSON: "+j)},noop:function(){},globalEval:function(j){if(j&& -l.test(j)){var s=t.getElementsByTagName("head")[0]||t.documentElement,v=t.createElement("script");v.type="text/javascript";if(b.support.scriptEval)v.appendChild(t.createTextNode(j));else v.text=j;s.insertBefore(v,s.firstChild);s.removeChild(v)}},nodeName:function(j,s){return j.nodeName&&j.nodeName.toUpperCase()===s.toUpperCase()},each:function(j,s,v){var z,H=0,G=j.length,K=G===B||b.isFunction(j);if(v)if(K)for(z in j){if(s.apply(j[z],v)===false)break}else for(;H
    a";var f=d.getElementsByTagName("*"),h=d.getElementsByTagName("a")[0],l=t.createElement("select"), -k=l.appendChild(t.createElement("option"));if(!(!f||!f.length||!h)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(h.getAttribute("style")),hrefNormalized:h.getAttribute("href")==="/a",opacity:/^0.55$/.test(h.style.opacity),cssFloat:!!h.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:k.selected,deleteExpando:true,optDisabled:false,checkClone:false, -scriptEval:false,noCloneEvent:true,boxModel:null,inlineBlockNeedsLayout:false,shrinkWrapBlocks:false,reliableHiddenOffsets:true};l.disabled=true;c.support.optDisabled=!k.disabled;b.type="text/javascript";try{b.appendChild(t.createTextNode("window."+e+"=1;"))}catch(o){}a.insertBefore(b,a.firstChild);if(E[e]){c.support.scriptEval=true;delete E[e]}try{delete b.test}catch(x){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function r(){c.support.noCloneEvent= -false;d.detachEvent("onclick",r)});d.cloneNode(true).fireEvent("onclick")}d=t.createElement("div");d.innerHTML="";a=t.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var r=t.createElement("div");r.style.width=r.style.paddingLeft="1px";t.body.appendChild(r);c.boxModel=c.support.boxModel=r.offsetWidth===2;if("zoom"in r.style){r.style.display="inline";r.style.zoom= -1;c.support.inlineBlockNeedsLayout=r.offsetWidth===2;r.style.display="";r.innerHTML="
    ";c.support.shrinkWrapBlocks=r.offsetWidth!==2}r.innerHTML="
    t
    ";var A=r.getElementsByTagName("td");c.support.reliableHiddenOffsets=A[0].offsetHeight===0;A[0].style.display="";A[1].style.display="none";c.support.reliableHiddenOffsets=c.support.reliableHiddenOffsets&&A[0].offsetHeight===0;r.innerHTML="";t.body.removeChild(r).style.display= -"none"});a=function(r){var A=t.createElement("div");r="on"+r;var C=r in A;if(!C){A.setAttribute(r,"return;");C=typeof A[r]==="function"}return C};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=f=h=null}})();var ra={},Ja=/^(?:\{.*\}|\[.*\])$/;c.extend({cache:{},uuid:0,expando:"jQuery"+c.now(),noData:{embed:true,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:true},data:function(a,b,d){if(c.acceptData(a)){a=a==E?ra:a;var e=a.nodeType,f=e?a[c.expando]:null,h= -c.cache;if(!(e&&!f&&typeof b==="string"&&d===B)){if(e)f||(a[c.expando]=f=++c.uuid);else h=a;if(typeof b==="object")if(e)h[f]=c.extend(h[f],b);else c.extend(h,b);else if(e&&!h[f])h[f]={};a=e?h[f]:h;if(d!==B)a[b]=d;return typeof b==="string"?a[b]:a}}},removeData:function(a,b){if(c.acceptData(a)){a=a==E?ra:a;var d=a.nodeType,e=d?a[c.expando]:a,f=c.cache,h=d?f[e]:e;if(b){if(h){delete h[b];d&&c.isEmptyObject(h)&&c.removeData(a)}}else if(d&&c.support.deleteExpando)delete a[c.expando];else if(a.removeAttribute)a.removeAttribute(c.expando); -else if(d)delete f[e];else for(var l in a)delete a[l]}},acceptData:function(a){if(a.nodeName){var b=c.noData[a.nodeName.toLowerCase()];if(b)return!(b===true||a.getAttribute("classid")!==b)}return true}});c.fn.extend({data:function(a,b){var d=null;if(typeof a==="undefined"){if(this.length){var e=this[0].attributes,f;d=c.data(this[0]);for(var h=0,l=e.length;h-1)return true;return false},val:function(a){if(!arguments.length){var b=this[0];if(b){if(c.nodeName(b,"option")){var d=b.attributes.value;return!d||d.specified?b.value:b.text}if(c.nodeName(b,"select")){var e=b.selectedIndex;d=[];var f=b.options;b=b.type==="select-one"; -if(e<0)return null;var h=b?e:0;for(e=b?e+1:f.length;h=0;else if(c.nodeName(this,"select")){var A=c.makeArray(r);c("option",this).each(function(){this.selected=c.inArray(c(this).val(),A)>=0});if(!A.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true}, -attr:function(a,b,d,e){if(!a||a.nodeType===3||a.nodeType===8)return B;if(e&&b in c.attrFn)return c(a)[b](d);e=a.nodeType!==1||!c.isXMLDoc(a);var f=d!==B;b=e&&c.props[b]||b;var h=Ta.test(b);if((b in a||a[b]!==B)&&e&&!h){if(f){b==="type"&&Ua.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed");if(d===null)a.nodeType===1&&a.removeAttribute(b);else a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&& -b.specified?b.value:Va.test(a.nodeName)||Wa.test(a.nodeName)&&a.href?0:B;return a[b]}if(!c.support.style&&e&&b==="style"){if(f)a.style.cssText=""+d;return a.style.cssText}f&&a.setAttribute(b,""+d);if(!a.attributes[b]&&a.hasAttribute&&!a.hasAttribute(b))return B;a=!c.support.hrefNormalized&&e&&h?a.getAttribute(b,2):a.getAttribute(b);return a===null?B:a}});var X=/\.(.*)$/,ia=/^(?:textarea|input|select)$/i,La=/\./g,Ma=/ /g,Xa=/[^\w\s.|`]/g,Ya=function(a){return a.replace(Xa,"\\$&")},ua={focusin:0,focusout:0}; -c.event={add:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(c.isWindow(a)&&a!==E&&!a.frameElement)a=E;if(d===false)d=U;else if(!d)return;var f,h;if(d.handler){f=d;d=f.handler}if(!d.guid)d.guid=c.guid++;if(h=c.data(a)){var l=a.nodeType?"events":"__events__",k=h[l],o=h.handle;if(typeof k==="function"){o=k.handle;k=k.events}else if(!k){a.nodeType||(h[l]=h=function(){});h.events=k={}}if(!o)h.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem, -arguments):B};o.elem=a;b=b.split(" ");for(var x=0,r;l=b[x++];){h=f?c.extend({},f):{handler:d,data:e};if(l.indexOf(".")>-1){r=l.split(".");l=r.shift();h.namespace=r.slice(0).sort().join(".")}else{r=[];h.namespace=""}h.type=l;if(!h.guid)h.guid=d.guid;var A=k[l],C=c.event.special[l]||{};if(!A){A=k[l]=[];if(!C.setup||C.setup.call(a,e,r,o)===false)if(a.addEventListener)a.addEventListener(l,o,false);else a.attachEvent&&a.attachEvent("on"+l,o)}if(C.add){C.add.call(a,h);if(!h.handler.guid)h.handler.guid= -d.guid}A.push(h);c.event.global[l]=true}a=null}}},global:{},remove:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(d===false)d=U;var f,h,l=0,k,o,x,r,A,C,J=a.nodeType?"events":"__events__",w=c.data(a),I=w&&w[J];if(w&&I){if(typeof I==="function"){w=I;I=I.events}if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(f in I)c.event.remove(a,f+b)}else{for(b=b.split(" ");f=b[l++];){r=f;k=f.indexOf(".")<0;o=[];if(!k){o=f.split(".");f=o.shift();x=RegExp("(^|\\.)"+ -c.map(o.slice(0).sort(),Ya).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(A=I[f])if(d){r=c.event.special[f]||{};for(h=e||0;h=0){a.type=f=f.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[f]&&c.each(c.cache,function(){this.events&&this.events[f]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType=== -8)return B;a.result=B;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(e=d.nodeType?c.data(d,"handle"):(c.data(d,"__events__")||{}).handle)&&e.apply(d,b);e=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+f]&&d["on"+f].apply(d,b)===false){a.result=false;a.preventDefault()}}catch(h){}if(!a.isPropagationStopped()&&e)c.event.trigger(a,b,e,true);else if(!a.isDefaultPrevented()){var l;e=a.target;var k=f.replace(X,""),o=c.nodeName(e,"a")&&k=== -"click",x=c.event.special[k]||{};if((!x._default||x._default.call(d,a)===false)&&!o&&!(e&&e.nodeName&&c.noData[e.nodeName.toLowerCase()])){try{if(e[k]){if(l=e["on"+k])e["on"+k]=null;c.event.triggered=true;e[k]()}}catch(r){}if(l)e["on"+k]=l;c.event.triggered=false}}},handle:function(a){var b,d,e,f;d=[];var h=c.makeArray(arguments);a=h[0]=c.event.fix(a||E.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive;if(!b){e=a.type.split(".");a.type=e.shift();d=e.slice(0).sort();e=RegExp("(^|\\.)"+ -d.join("\\.(?:.*\\.)?")+"(\\.|$)")}a.namespace=a.namespace||d.join(".");f=c.data(this,this.nodeType?"events":"__events__");if(typeof f==="function")f=f.events;d=(f||{})[a.type];if(f&&d){d=d.slice(0);f=0;for(var l=d.length;f-1?c.map(a.options,function(e){return e.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},Z=function(a,b){var d=a.target,e,f;if(!(!ia.test(d.nodeName)||d.readOnly)){e=c.data(d,"_change_data");f=xa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",f);if(!(e===B||f===e))if(e!=null||f){a.type="change";a.liveFired= -B;return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:Z,beforedeactivate:Z,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return Z.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return Z.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a,"_change_data",xa(a))}},setup:function(){if(this.type=== -"file")return false;for(var a in V)c.event.add(this,a+".specialChange",V[a]);return ia.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return ia.test(this.nodeName)}};V=c.event.special.change.filters;V.focus=V.beforeactivate}t.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(e){e=c.event.fix(e);e.type=b;return c.event.trigger(e,null,e.target)}c.event.special[b]={setup:function(){ua[b]++===0&&t.addEventListener(a,d,true)},teardown:function(){--ua[b]=== -0&&t.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,e,f){if(typeof d==="object"){for(var h in d)this[b](h,e,d[h],f);return this}if(c.isFunction(e)||e===false){f=e;e=B}var l=b==="one"?c.proxy(f,function(o){c(this).unbind(o,l);return f.apply(this,arguments)}):f;if(d==="unload"&&b!=="one")this.one(d,e,f);else{h=0;for(var k=this.length;h0?this.bind(b,d,e):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});E.attachEvent&&!E.addEventListener&&c(E).bind("unload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}}); -(function(){function a(g,i,n,m,p,q){p=0;for(var u=m.length;p0){F=y;break}}y=y[g]}m[p]=F}}}var d=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,f=Object.prototype.toString,h=false,l=true;[0,0].sort(function(){l=false;return 0});var k=function(g,i,n,m){n=n||[];var p=i=i||t;if(i.nodeType!==1&&i.nodeType!==9)return[];if(!g||typeof g!=="string")return n;var q,u,y,F,M,N=true,O=k.isXML(i),D=[],R=g;do{d.exec("");if(q=d.exec(R)){R=q[3];D.push(q[1]);if(q[2]){F=q[3]; -break}}}while(q);if(D.length>1&&x.exec(g))if(D.length===2&&o.relative[D[0]])u=L(D[0]+D[1],i);else for(u=o.relative[D[0]]?[i]:k(D.shift(),i);D.length;){g=D.shift();if(o.relative[g])g+=D.shift();u=L(g,u)}else{if(!m&&D.length>1&&i.nodeType===9&&!O&&o.match.ID.test(D[0])&&!o.match.ID.test(D[D.length-1])){q=k.find(D.shift(),i,O);i=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]}if(i){q=m?{expr:D.pop(),set:C(m)}:k.find(D.pop(),D.length===1&&(D[0]==="~"||D[0]==="+")&&i.parentNode?i.parentNode:i,O);u=q.expr?k.filter(q.expr, -q.set):q.set;if(D.length>0)y=C(u);else N=false;for(;D.length;){q=M=D.pop();if(o.relative[M])q=D.pop();else M="";if(q==null)q=i;o.relative[M](y,q,O)}}else y=[]}y||(y=u);y||k.error(M||g);if(f.call(y)==="[object Array]")if(N)if(i&&i.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&k.contains(i,y[g])))n.push(u[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&n.push(u[g]);else n.push.apply(n,y);else C(y,n);if(F){k(F,p,n,m);k.uniqueSort(n)}return n};k.uniqueSort=function(g){if(w){h= -l;g.sort(w);if(h)for(var i=1;i0};k.find=function(g,i,n){var m;if(!g)return[];for(var p=0,q=o.order.length;p":function(g,i){var n,m=typeof i==="string",p=0,q=g.length;if(m&&!/\W/.test(i))for(i=i.toLowerCase();p=0))n||m.push(u);else if(n)i[q]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},CHILD:function(g){if(g[1]==="nth"){var i=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=i[1]+(i[2]||1)-0;g[3]=i[3]-0}g[0]=e++;return g},ATTR:function(g,i,n, -m,p,q){i=g[1].replace(/\\/g,"");if(!q&&o.attrMap[i])g[1]=o.attrMap[i];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,i,n,m,p){if(g[1]==="not")if((d.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,i);else{g=k.filter(g[3],i,n,true^p);n||m.push.apply(m,g);return false}else if(o.match.POS.test(g[0])||o.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled=== -true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,i,n){return!!k(n[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"=== -g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},setFilters:{first:function(g,i){return i===0},last:function(g,i,n,m){return i===m.length-1},even:function(g,i){return i%2===0},odd:function(g,i){return i%2===1},lt:function(g,i,n){return in[3]-0},nth:function(g,i,n){return n[3]- -0===i},eq:function(g,i,n){return n[3]-0===i}},filter:{PSEUDO:function(g,i,n,m){var p=i[1],q=o.filters[p];if(q)return q(g,n,i,m);else if(p==="contains")return(g.textContent||g.innerText||k.getText([g])||"").indexOf(i[3])>=0;else if(p==="not"){i=i[3];n=0;for(m=i.length;n=0}},ID:function(g,i){return g.nodeType===1&&g.getAttribute("id")===i},TAG:function(g,i){return i==="*"&&g.nodeType===1||g.nodeName.toLowerCase()=== -i},CLASS:function(g,i){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(i)>-1},ATTR:function(g,i){var n=i[1];n=o.attrHandle[n]?o.attrHandle[n](g):g[n]!=null?g[n]:g.getAttribute(n);var m=n+"",p=i[2],q=i[4];return n==null?p==="!=":p==="="?m===q:p==="*="?m.indexOf(q)>=0:p==="~="?(" "+m+" ").indexOf(q)>=0:!q?m&&n!==false:p==="!="?m!==q:p==="^="?m.indexOf(q)===0:p==="$="?m.substr(m.length-q.length)===q:p==="|="?m===q||m.substr(0,q.length+1)===q+"-":false},POS:function(g,i,n,m){var p=o.setFilters[i[2]]; -if(p)return p(g,n,i,m)}}},x=o.match.POS,r=function(g,i){return"\\"+(i-0+1)},A;for(A in o.match){o.match[A]=RegExp(o.match[A].source+/(?![^\[]*\])(?![^\(]*\))/.source);o.leftMatch[A]=RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[A].source.replace(/\\(\d+)/g,r))}var C=function(g,i){g=Array.prototype.slice.call(g,0);if(i){i.push.apply(i,g);return i}return g};try{Array.prototype.slice.call(t.documentElement.childNodes,0)}catch(J){C=function(g,i){var n=0,m=i||[];if(f.call(g)==="[object Array]")Array.prototype.push.apply(m, -g);else if(typeof g.length==="number")for(var p=g.length;n";n.insertBefore(g,n.firstChild);if(t.getElementById(i)){o.find.ID=function(m,p,q){if(typeof p.getElementById!=="undefined"&&!q)return(p=p.getElementById(m[1]))?p.id===m[1]||typeof p.getAttributeNode!=="undefined"&&p.getAttributeNode("id").nodeValue===m[1]?[p]:B:[]};o.filter.ID=function(m,p){var q=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&q&&q.nodeValue===p}}n.removeChild(g); -n=g=null})();(function(){var g=t.createElement("div");g.appendChild(t.createComment(""));if(g.getElementsByTagName("*").length>0)o.find.TAG=function(i,n){var m=n.getElementsByTagName(i[1]);if(i[1]==="*"){for(var p=[],q=0;m[q];q++)m[q].nodeType===1&&p.push(m[q]);m=p}return m};g.innerHTML="";if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")o.attrHandle.href=function(i){return i.getAttribute("href",2)};g=null})();t.querySelectorAll&& -function(){var g=k,i=t.createElement("div");i.innerHTML="

    ";if(!(i.querySelectorAll&&i.querySelectorAll(".TEST").length===0)){k=function(m,p,q,u){p=p||t;m=m.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!u&&!k.isXML(p))if(p.nodeType===9)try{return C(p.querySelectorAll(m),q)}catch(y){}else if(p.nodeType===1&&p.nodeName.toLowerCase()!=="object"){var F=p.getAttribute("id"),M=F||"__sizzle__";F||p.setAttribute("id",M);try{return C(p.querySelectorAll("#"+M+" "+m),q)}catch(N){}finally{F|| -p.removeAttribute("id")}}return g(m,p,q,u)};for(var n in g)k[n]=g[n];i=null}}();(function(){var g=t.documentElement,i=g.matchesSelector||g.mozMatchesSelector||g.webkitMatchesSelector||g.msMatchesSelector,n=false;try{i.call(t.documentElement,"[test!='']:sizzle")}catch(m){n=true}if(i)k.matchesSelector=function(p,q){q=q.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(p))try{if(n||!o.match.PSEUDO.test(q)&&!/!=/.test(q))return i.call(p,q)}catch(u){}return k(q,null,null,[p]).length>0}})();(function(){var g= -t.createElement("div");g.innerHTML="
    ";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){o.order.splice(1,0,"CLASS");o.find.CLASS=function(i,n,m){if(typeof n.getElementsByClassName!=="undefined"&&!m)return n.getElementsByClassName(i[1])};g=null}}})();k.contains=t.documentElement.contains?function(g,i){return g!==i&&(g.contains?g.contains(i):true)}:t.documentElement.compareDocumentPosition? -function(g,i){return!!(g.compareDocumentPosition(i)&16)}:function(){return false};k.isXML=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false};var L=function(g,i){for(var n,m=[],p="",q=i.nodeType?[i]:i;n=o.match.PSEUDO.exec(g);){p+=n[0];g=g.replace(o.match.PSEUDO,"")}g=o.relative[g]?g+"*":g;n=0;for(var u=q.length;n0)for(var h=d;h0},closest:function(a,b){var d=[],e,f,h=this[0];if(c.isArray(a)){var l,k={},o=1;if(h&&a.length){e=0;for(f=a.length;e-1:c(h).is(e))d.push({selector:l,elem:h,level:o})}h= -h.parentNode;o++}}return d}l=cb.test(a)?c(a,b||this.context):null;e=0;for(f=this.length;e-1:c.find.matchesSelector(h,a)){d.push(h);break}else{h=h.parentNode;if(!h||!h.ownerDocument||h===b)break}d=d.length>1?c.unique(d):d;return this.pushStack(d,"closest",a)},index:function(a){if(!a||typeof a==="string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var d=typeof a==="string"?c(a,b||this.context): -c.makeArray(a),e=c.merge(this.get(),d);return this.pushStack(!d[0]||!d[0].parentNode||d[0].parentNode.nodeType===11||!e[0]||!e[0].parentNode||e[0].parentNode.nodeType===11?e:c.unique(e))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a, -2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a, -b){c.fn[a]=function(d,e){var f=c.map(this,b,d);Za.test(a)||(e=d);if(e&&typeof e==="string")f=c.filter(e,f);f=this.length>1?c.unique(f):f;if((this.length>1||ab.test(e))&&$a.test(a))f=f.reverse();return this.pushStack(f,a,bb.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return b.length===1?c.find.matchesSelector(b[0],a)?[b[0]]:[]:c.find.matches(a,b)},dir:function(a,b,d){var e=[];for(a=a[b];a&&a.nodeType!==9&&(d===B||a.nodeType!==1||!c(a).is(d));){a.nodeType===1&& -e.push(a);a=a[b]}return e},nth:function(a,b,d){b=b||1;for(var e=0;a;a=a[d])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var za=/ jQuery\d+="(?:\d+|null)"/g,$=/^\s+/,Aa=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Ba=/<([\w:]+)/,db=/\s]+\/)>/g,P={option:[1, -""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]};P.optgroup=P.option;P.tbody=P.tfoot=P.colgroup=P.caption=P.thead;P.th=P.td;if(!c.support.htmlSerialize)P._default=[1,"div
    ","
    "];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d= -c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==B)return this.empty().append((this[0]&&this[0].ownerDocument||t).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this}, -wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})}, -prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b, -this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,e;(e=this[d])!=null;d++)if(!a||c.filter(a,[e]).length){if(!b&&e.nodeType===1){c.cleanData(e.getElementsByTagName("*"));c.cleanData([e])}e.parentNode&&e.parentNode.removeChild(e)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild); -return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,e=this.ownerDocument;if(!d){d=e.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(za,"").replace(fb,'="$1">').replace($,"")],e)[0]}else return this.cloneNode(true)});if(a===true){na(this,b);na(this.find("*"),b.find("*"))}return b},html:function(a){if(a===B)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(za,""):null; -else if(typeof a==="string"&&!Ca.test(a)&&(c.support.leadingWhitespace||!$.test(a))&&!P[(Ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Aa,"<$1>");try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?h.cloneNode(true):h)}k.length&&c.each(k,Oa)}return this}});c.buildFragment=function(a,b,d){var e,f,h;b=b&&b[0]?b[0].ownerDocument||b[0]:t;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&b===t&&!Ca.test(a[0])&&(c.support.checkClone||!Da.test(a[0]))){f=true;if(h=c.fragments[a[0]])if(h!==1)e=h}if(!e){e=b.createDocumentFragment();c.clean(a,b,e,d)}if(f)c.fragments[a[0]]=h?e:1;return{fragment:e,cacheable:f}};c.fragments={};c.each({appendTo:"append", -prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var e=[];d=c(d);var f=this.length===1&&this[0].parentNode;if(f&&f.nodeType===11&&f.childNodes.length===1&&d.length===1){d[b](this[0]);return this}else{f=0;for(var h=d.length;f0?this.clone(true):this).get();c(d[f])[b](l);e=e.concat(l)}return this.pushStack(e,a,d.selector)}}});c.extend({clean:function(a,b,d,e){b=b||t;if(typeof b.createElement==="undefined")b=b.ownerDocument|| -b[0]&&b[0].ownerDocument||t;for(var f=[],h=0,l;(l=a[h])!=null;h++){if(typeof l==="number")l+="";if(l){if(typeof l==="string"&&!eb.test(l))l=b.createTextNode(l);else if(typeof l==="string"){l=l.replace(Aa,"<$1>");var k=(Ba.exec(l)||["",""])[1].toLowerCase(),o=P[k]||P._default,x=o[0],r=b.createElement("div");for(r.innerHTML=o[1]+l+o[2];x--;)r=r.lastChild;if(!c.support.tbody){x=db.test(l);k=k==="table"&&!x?r.firstChild&&r.firstChild.childNodes:o[1]===""&&!x?r.childNodes:[];for(o=k.length- -1;o>=0;--o)c.nodeName(k[o],"tbody")&&!k[o].childNodes.length&&k[o].parentNode.removeChild(k[o])}!c.support.leadingWhitespace&&$.test(l)&&r.insertBefore(b.createTextNode($.exec(l)[0]),r.firstChild);l=r.childNodes}if(l.nodeType)f.push(l);else f=c.merge(f,l)}}if(d)for(h=0;f[h];h++)if(e&&c.nodeName(f[h],"script")&&(!f[h].type||f[h].type.toLowerCase()==="text/javascript"))e.push(f[h].parentNode?f[h].parentNode.removeChild(f[h]):f[h]);else{f[h].nodeType===1&&f.splice.apply(f,[h+1,0].concat(c.makeArray(f[h].getElementsByTagName("script")))); -d.appendChild(f[h])}return f},cleanData:function(a){for(var b,d,e=c.cache,f=c.event.special,h=c.support.deleteExpando,l=0,k;(k=a[l])!=null;l++)if(!(k.nodeName&&c.noData[k.nodeName.toLowerCase()]))if(d=k[c.expando]){if((b=e[d])&&b.events)for(var o in b.events)f[o]?c.event.remove(k,o):c.removeEvent(k,o,b.handle);if(h)delete k[c.expando];else k.removeAttribute&&k.removeAttribute(c.expando);delete e[d]}}});var Ea=/alpha\([^)]*\)/i,gb=/opacity=([^)]*)/,hb=/-([a-z])/ig,ib=/([A-Z])/g,Fa=/^-?\d+(?:px)?$/i, -jb=/^-?\d/,kb={position:"absolute",visibility:"hidden",display:"block"},Pa=["Left","Right"],Qa=["Top","Bottom"],W,Ga,aa,lb=function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){if(arguments.length===2&&b===B)return this;return c.access(this,a,b,true,function(d,e,f){return f!==B?c.style(d,e,f):c.css(d,e)})};c.extend({cssHooks:{opacity:{get:function(a,b){if(b){var d=W(a,"opacity","opacity");return d===""?"1":d}else return a.style.opacity}}},cssNumber:{zIndex:true,fontWeight:true,opacity:true, -zoom:true,lineHeight:true},cssProps:{"float":c.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,d,e){if(!(!a||a.nodeType===3||a.nodeType===8||!a.style)){var f,h=c.camelCase(b),l=a.style,k=c.cssHooks[h];b=c.cssProps[h]||h;if(d!==B){if(!(typeof d==="number"&&isNaN(d)||d==null)){if(typeof d==="number"&&!c.cssNumber[h])d+="px";if(!k||!("set"in k)||(d=k.set(a,d))!==B)try{l[b]=d}catch(o){}}}else{if(k&&"get"in k&&(f=k.get(a,false,e))!==B)return f;return l[b]}}},css:function(a,b,d){var e,f=c.camelCase(b), -h=c.cssHooks[f];b=c.cssProps[f]||f;if(h&&"get"in h&&(e=h.get(a,true,d))!==B)return e;else if(W)return W(a,b,f)},swap:function(a,b,d){var e={},f;for(f in b){e[f]=a.style[f];a.style[f]=b[f]}d.call(a);for(f in b)a.style[f]=e[f]},camelCase:function(a){return a.replace(hb,lb)}});c.curCSS=c.css;c.each(["height","width"],function(a,b){c.cssHooks[b]={get:function(d,e,f){var h;if(e){if(d.offsetWidth!==0)h=oa(d,b,f);else c.swap(d,kb,function(){h=oa(d,b,f)});if(h<=0){h=W(d,b,b);if(h==="0px"&&aa)h=aa(d,b,b); -if(h!=null)return h===""||h==="auto"?"0px":h}if(h<0||h==null){h=d.style[b];return h===""||h==="auto"?"0px":h}return typeof h==="string"?h:h+"px"}},set:function(d,e){if(Fa.test(e)){e=parseFloat(e);if(e>=0)return e+"px"}else return e}}});if(!c.support.opacity)c.cssHooks.opacity={get:function(a,b){return gb.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var d=a.style;d.zoom=1;var e=c.isNaN(b)?"":"alpha(opacity="+b*100+")",f= -d.filter||"";d.filter=Ea.test(f)?f.replace(Ea,e):d.filter+" "+e}};if(t.defaultView&&t.defaultView.getComputedStyle)Ga=function(a,b,d){var e;d=d.replace(ib,"-$1").toLowerCase();if(!(b=a.ownerDocument.defaultView))return B;if(b=b.getComputedStyle(a,null)){e=b.getPropertyValue(d);if(e===""&&!c.contains(a.ownerDocument.documentElement,a))e=c.style(a,d)}return e};if(t.documentElement.currentStyle)aa=function(a,b){var d,e,f=a.currentStyle&&a.currentStyle[b],h=a.style;if(!Fa.test(f)&&jb.test(f)){d=h.left; -e=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;h.left=b==="fontSize"?"1em":f||0;f=h.pixelLeft+"px";h.left=d;a.runtimeStyle.left=e}return f===""?"auto":f};W=Ga||aa;if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=a.offsetHeight;return a.offsetWidth===0&&b===0||!c.support.reliableHiddenOffsets&&(a.style.display||c.css(a,"display"))==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var mb=c.now(),nb=/)<[^<]*)*<\/script>/gi, -ob=/^(?:select|textarea)/i,pb=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,qb=/^(?:GET|HEAD)$/,Ra=/\[\]$/,T=/\=\?(&|$)/,ja=/\?/,rb=/([?&])_=[^&]*/,sb=/^(\w+:)?\/\/([^\/?#]+)/,tb=/%20/g,ub=/#.*$/,Ha=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!=="string"&&Ha)return Ha.apply(this,arguments);else if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var f=a.slice(e,a.length);a=a.slice(0,e)}e="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b=== -"object"){b=c.param(b,c.ajaxSettings.traditional);e="POST"}var h=this;c.ajax({url:a,type:e,dataType:"html",data:b,complete:function(l,k){if(k==="success"||k==="notmodified")h.html(f?c("
    ").append(l.responseText.replace(nb,"")).find(f):l.responseText);d&&h.each(d,[l.responseText,k,l])}});return this},serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&& -!this.disabled&&(this.checked||ob.test(this.nodeName)||pb.test(this.type))}).map(function(a,b){var d=c(this).val();return d==null?null:c.isArray(d)?c.map(d,function(e){return{name:b.name,value:e}}):{name:b.name,value:d}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:e})}, -getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:e})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return new E.XMLHttpRequest},accepts:{xml:"application/xml, text/xml",html:"text/html", -script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},ajax:function(a){var b=c.extend(true,{},c.ajaxSettings,a),d,e,f,h=b.type.toUpperCase(),l=qb.test(h);b.url=b.url.replace(ub,"");b.context=a&&a.context!=null?a.context:b;if(b.data&&b.processData&&typeof b.data!=="string")b.data=c.param(b.data,b.traditional);if(b.dataType==="jsonp"){if(h==="GET")T.test(b.url)||(b.url+=(ja.test(b.url)?"&":"?")+(b.jsonp||"callback")+"=?");else if(!b.data|| -!T.test(b.data))b.data=(b.data?b.data+"&":"")+(b.jsonp||"callback")+"=?";b.dataType="json"}if(b.dataType==="json"&&(b.data&&T.test(b.data)||T.test(b.url))){d=b.jsonpCallback||"jsonp"+mb++;if(b.data)b.data=(b.data+"").replace(T,"="+d+"$1");b.url=b.url.replace(T,"="+d+"$1");b.dataType="script";var k=E[d];E[d]=function(m){if(c.isFunction(k))k(m);else{E[d]=B;try{delete E[d]}catch(p){}}f=m;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);r&&r.removeChild(A)}}if(b.dataType==="script"&&b.cache===null)b.cache= -false;if(b.cache===false&&l){var o=c.now(),x=b.url.replace(rb,"$1_="+o);b.url=x+(x===b.url?(ja.test(b.url)?"&":"?")+"_="+o:"")}if(b.data&&l)b.url+=(ja.test(b.url)?"&":"?")+b.data;b.global&&c.active++===0&&c.event.trigger("ajaxStart");o=(o=sb.exec(b.url))&&(o[1]&&o[1].toLowerCase()!==location.protocol||o[2].toLowerCase()!==location.host);if(b.dataType==="script"&&h==="GET"&&o){var r=t.getElementsByTagName("head")[0]||t.documentElement,A=t.createElement("script");if(b.scriptCharset)A.charset=b.scriptCharset; -A.src=b.url;if(!d){var C=false;A.onload=A.onreadystatechange=function(){if(!C&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){C=true;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);A.onload=A.onreadystatechange=null;r&&A.parentNode&&r.removeChild(A)}}}r.insertBefore(A,r.firstChild);return B}var J=false,w=b.xhr();if(w){b.username?w.open(h,b.url,b.async,b.username,b.password):w.open(h,b.url,b.async);try{if(b.data!=null&&!l||a&&a.contentType)w.setRequestHeader("Content-Type", -b.contentType);if(b.ifModified){c.lastModified[b.url]&&w.setRequestHeader("If-Modified-Since",c.lastModified[b.url]);c.etag[b.url]&&w.setRequestHeader("If-None-Match",c.etag[b.url])}o||w.setRequestHeader("X-Requested-With","XMLHttpRequest");w.setRequestHeader("Accept",b.dataType&&b.accepts[b.dataType]?b.accepts[b.dataType]+", */*; q=0.01":b.accepts._default)}catch(I){}if(b.beforeSend&&b.beforeSend.call(b.context,w,b)===false){b.global&&c.active--===1&&c.event.trigger("ajaxStop");w.abort();return false}b.global&& -c.triggerGlobal(b,"ajaxSend",[w,b]);var L=w.onreadystatechange=function(m){if(!w||w.readyState===0||m==="abort"){J||c.handleComplete(b,w,e,f);J=true;if(w)w.onreadystatechange=c.noop}else if(!J&&w&&(w.readyState===4||m==="timeout")){J=true;w.onreadystatechange=c.noop;e=m==="timeout"?"timeout":!c.httpSuccess(w)?"error":b.ifModified&&c.httpNotModified(w,b.url)?"notmodified":"success";var p;if(e==="success")try{f=c.httpData(w,b.dataType,b)}catch(q){e="parsererror";p=q}if(e==="success"||e==="notmodified")d|| -c.handleSuccess(b,w,e,f);else c.handleError(b,w,e,p);d||c.handleComplete(b,w,e,f);m==="timeout"&&w.abort();if(b.async)w=null}};try{var g=w.abort;w.abort=function(){w&&Function.prototype.call.call(g,w);L("abort")}}catch(i){}b.async&&b.timeout>0&&setTimeout(function(){w&&!J&&L("timeout")},b.timeout);try{w.send(l||b.data==null?null:b.data)}catch(n){c.handleError(b,w,null,n);c.handleComplete(b,w,e,f)}b.async||L();return w}},param:function(a,b){var d=[],e=function(h,l){l=c.isFunction(l)?l():l;d[d.length]= -encodeURIComponent(h)+"="+encodeURIComponent(l)};if(b===B)b=c.ajaxSettings.traditional;if(c.isArray(a)||a.jquery)c.each(a,function(){e(this.name,this.value)});else for(var f in a)da(f,a[f],b,e);return d.join("&").replace(tb,"+")}});c.extend({active:0,lastModified:{},etag:{},handleError:function(a,b,d,e){a.error&&a.error.call(a.context,b,d,e);a.global&&c.triggerGlobal(a,"ajaxError",[b,a,e])},handleSuccess:function(a,b,d,e){a.success&&a.success.call(a.context,e,d,b);a.global&&c.triggerGlobal(a,"ajaxSuccess", -[b,a])},handleComplete:function(a,b,d){a.complete&&a.complete.call(a.context,b,d);a.global&&c.triggerGlobal(a,"ajaxComplete",[b,a]);a.global&&c.active--===1&&c.event.trigger("ajaxStop")},triggerGlobal:function(a,b,d){(a.context&&a.context.url==null?c(a.context):c.event).trigger(b,d)},httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===1223}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"), -e=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(e)c.etag[b]=e;return a.status===304},httpData:function(a,b,d){var e=a.getResponseHeader("content-type")||"",f=b==="xml"||!b&&e.indexOf("xml")>=0;a=f?a.responseXML:a.responseText;f&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b==="json"||!b&&e.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&e.indexOf("javascript")>=0)c.globalEval(a);return a}}); -if(E.ActiveXObject)c.ajaxSettings.xhr=function(){if(E.location.protocol!=="file:")try{return new E.XMLHttpRequest}catch(a){}try{return new E.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}};c.support.ajax=!!c.ajaxSettings.xhr();var ea={},vb=/^(?:toggle|show|hide)$/,wb=/^([+\-]=)?([\d+.\-]+)(.*)$/,ba,pa=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b,d){if(a||a===0)return this.animate(S("show", -3),a,b,d);else{d=0;for(var e=this.length;d=0;e--)if(d[e].elem===this){b&&d[e](true);d.splice(e,1)}});b||this.dequeue();return this}});c.each({slideDown:S("show",1),slideUp:S("hide",1),slideToggle:S("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){c.fn[a]=function(d,e,f){return this.animate(b, -d,e,f)}});c.extend({speed:function(a,b,d){var e=a&&typeof a==="object"?c.extend({},a):{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};e.duration=c.fx.off?0:typeof e.duration==="number"?e.duration:e.duration in c.fx.speeds?c.fx.speeds[e.duration]:c.fx.speeds._default;e.old=e.complete;e.complete=function(){e.queue!==false&&c(this).dequeue();c.isFunction(e.old)&&e.old.call(this)};return e},easing:{linear:function(a,b,d,e){return d+e*a},swing:function(a,b,d,e){return(-Math.cos(a* -Math.PI)/2+0.5)*e+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||c.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a=parseFloat(c.css(this.elem,this.prop));return a&&a>-1E4?a:0},custom:function(a,b,d){function e(l){return f.step(l)} -var f=this,h=c.fx;this.startTime=c.now();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;this.pos=this.state=0;e.elem=this.elem;if(e()&&c.timers.push(e)&&!ba)ba=setInterval(h.tick,h.interval)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true; -this.custom(this.cur(),0)},step:function(a){var b=c.now(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var e in this.options.curAnim)if(this.options.curAnim[e]!==true)d=false;if(d){if(this.options.overflow!=null&&!c.support.shrinkWrapBlocks){var f=this.elem,h=this.options;c.each(["","X","Y"],function(k,o){f.style["overflow"+o]=h.overflow[k]})}this.options.hide&&c(this.elem).hide();if(this.options.hide|| -this.options.show)for(var l in this.options.curAnim)c.style(this.elem,l,this.options.orig[l]);this.options.complete.call(this.elem)}return false}else{a=b-this.startTime;this.state=a/this.options.duration;b=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||b](this.state,a,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a= -c.timers,b=0;b-1;e={};var x={};if(o)x=f.position();l=o?x.top:parseInt(l,10)||0;k=o?x.left:parseInt(k,10)||0;if(c.isFunction(b))b=b.call(a,d,h);if(b.top!=null)e.top=b.top-h.top+l;if(b.left!=null)e.left=b.left-h.left+k;"using"in b?b.using.call(a, -e):f.css(e)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),e=Ia.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.css(a,"marginTop"))||0;d.left-=parseFloat(c.css(a,"marginLeft"))||0;e.top+=parseFloat(c.css(b[0],"borderTopWidth"))||0;e.left+=parseFloat(c.css(b[0],"borderLeftWidth"))||0;return{top:d.top-e.top,left:d.left-e.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||t.body;a&&!Ia.test(a.nodeName)&& -c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(e){var f=this[0],h;if(!f)return null;if(e!==B)return this.each(function(){if(h=fa(this))h.scrollTo(!a?e:c(h).scrollLeft(),a?e:c(h).scrollTop());else this[d]=e});else return(h=fa(f))?"pageXOffset"in h?h[a?"pageYOffset":"pageXOffset"]:c.support.boxModel&&h.document.documentElement[d]||h.document.body[d]:f[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase(); -c.fn["inner"+b]=function(){return this[0]?parseFloat(c.css(this[0],d,"padding")):null};c.fn["outer"+b]=function(e){return this[0]?parseFloat(c.css(this[0],d,e?"margin":"border")):null};c.fn[d]=function(e){var f=this[0];if(!f)return e==null?null:this;if(c.isFunction(e))return this.each(function(l){var k=c(this);k[d](e.call(this,l,k[d]()))});if(c.isWindow(f))return f.document.compatMode==="CSS1Compat"&&f.document.documentElement["client"+b]||f.document.body["client"+b];else if(f.nodeType===9)return Math.max(f.documentElement["client"+ -b],f.body["scroll"+b],f.documentElement["scroll"+b],f.body["offset"+b],f.documentElement["offset"+b]);else if(e===B){f=c.css(f,d);var h=parseFloat(f);return c.isNaN(h)?f:h}else return this.css(d,typeof e==="string"?e:e+"px")}})})(window); diff --git a/sitestatic/jquery-1.7.2.min.js b/sitestatic/jquery-1.7.2.min.js new file mode 100644 index 00000000..16ad06c5 --- /dev/null +++ b/sitestatic/jquery-1.7.2.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.7.2 jquery.com | jquery.org/license */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"":"")+""),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;e=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
    a",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="
    "+""+"
    ",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="
    t
    ",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="
    ",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function( +a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&j.push({elem:this,matches:d.slice(e)});for(k=0;k0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

    ";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
    ";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*",""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
    ","
    "]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f +.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(;d1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]===""&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
    ").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/sitestatic/jquery.tablesorter-2.3.11.js b/sitestatic/jquery.tablesorter-2.3.11.js new file mode 100644 index 00000000..e52e66dc --- /dev/null +++ b/sitestatic/jquery.tablesorter-2.3.11.js @@ -0,0 +1,1181 @@ +/*! +* TableSorter 2.3.11 - Client-side table sorting with ease! +* @requires jQuery v1.2.6+ +* +* Copyright (c) 2007 Christian Bach +* Examples and docs at: http://tablesorter.com +* Dual licensed under the MIT and GPL licenses: +* http://www.opensource.org/licenses/mit-license.php +* http://www.gnu.org/licenses/gpl.html +* +* @type jQuery +* @name tablesorter +* @cat Plugins/Tablesorter +* @author Christian Bach/christian.bach@polyester.se +* @contributor Rob Garrison/https://github.com/Mottie/tablesorter +*/ +!(function($) { + $.extend({ + tablesorter: new function() { + + this.version = "2.3.11"; + + var parsers = [], widgets = []; + this.defaults = { + + // appearance + widthFixed : false, // adds colgroup to fix widths of columns + + // functionality + cancelSelection : true, // prevent text selection in the header + dateFormat : "mmddyyyy", // other options: "ddmmyyy" or "yyyymmdd" + sortMultiSortKey : "shiftKey", // key used to select additional columns + usNumberFormat : true, // false for German "1.234.567,89" or French "1 234 567,89" + delayInit : false, // if false, the parsed table contents will not update until the first sort. + + // sort options + headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc. + ignoreCase : true, // ignore case while sorting + sortForce : null, // column(s) first sorted; always applied + sortList : [], // Initial sort order; applied initially; updated when manually sorted + sortAppend : null, // column(s) sorted last; always applied + + sortInitialOrder : "asc", // sort direction on first click + sortLocaleCompare: false, // replace equivalent character (accented characters) + sortReset : false, // third click on the header will reset column to default - unsorted + sortRestart : false, // restart sort to "sortInitialOrder" when clicking on previously unsorted columns + + emptyTo : "bottom", // sort empty cell to bottom, top, none, zero + stringTo : "max", // sort strings in numerical column as max, min, top, bottom, zero + textExtraction : "simple", // text extraction method/function - function(node, table, cellIndex){} + textSorter : null, // use custom text sorter - function(a,b){ return a.sort(b); } // basic sort + + // widget options + widgets: [], // method to add widgets, e.g. widgets: ['zebra'] + widgetOptions : { + zebra : [ "even", "odd" ] // zebra widget alternating row class names + }, + initWidgets : true, // apply widgets on tablesorter initialization + + // callbacks + initialized : null, // function(table){}, + onRenderHeader : null, // function(index){}, + + // css class names + tableClass : 'tablesorter', + cssAsc : "tablesorter-headerSortUp", + cssChildRow : "expand-child", + cssDesc : "tablesorter-headerSortDown", + cssHeader : "tablesorter-header", + cssInfoBlock : "tablesorter-infoOnly", // don't sort tbody with this class name + + // selectors + selectorHeaders : '> thead th', + selectorRemove : "tr.remove-me", + + // advanced + debug : false, + + // Internal variables + headerList: [], + empties: {}, + strings: {}, + parsers: [] + + // deprecated; but retained for backwards compatibility + // widgetZebra: { css: ["even", "odd"] } + + }; + + /* debuging utils */ + function log(s) { + if (typeof console !== "undefined" && typeof console.log !== "undefined") { + console.log(s); + } else { + alert(s); + } + } + + function benchmark(s, d) { + log(s + " (" + (new Date().getTime() - d.getTime()) + "ms)"); + } + + this.benchmark = benchmark; + this.hasInitialized = false; + + function getElementText(table, node, cellIndex) { + if (!node) { return ""; } + var c = table.config, + t = c.textExtraction, text = ""; + if (t === "simple") { + if (c.supportsTextContent) { + text = node.textContent; // newer browsers support this + } else { + text = $(node).text(); + } + } else { + if (typeof(t) === "function") { + text = t(node, table, cellIndex); + } else if (typeof(t) === "object" && t.hasOwnProperty(cellIndex)) { + text = t[cellIndex](node, table, cellIndex); + } else { + text = c.supportsTextContent ? node.textContent : $(node).text(); + } + } + return $.trim(text); + } + + /* parsers utils */ + function getParserById(name) { + var i, l = parsers.length; + for (i = 0; i < l; i++) { + if (parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) { + return parsers[i]; + } + } + return false; + } + + function detectParserForColumn(table, rows, rowIndex, cellIndex) { + var i, l = parsers.length, + node = false, + nodeValue = '', + keepLooking = true; + while (nodeValue === '' && keepLooking) { + rowIndex++; + if (rows[rowIndex]) { + node = rows[rowIndex].cells[cellIndex]; + nodeValue = getElementText(table, node, cellIndex); + if (table.config.debug) { + log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': ' + nodeValue); + } + } else { + keepLooking = false; + } + } + for (i = 1; i < l; i++) { + if (parsers[i].is(nodeValue, table, node)) { + return parsers[i]; + } + } + // 0 is always the generic parser (text) + return parsers[0]; + } + + function buildParserCache(table, $headers) { + var c = table.config, + tb = $(table.tBodies).filter(':not(.' + c.cssInfoBlock + ')'), + ts = $.tablesorter, rows, list, l, i, h, m, ch, cl, p, parsersDebug = ""; + if ( tb.length === 0) { return; } // In the case of empty tables + rows = tb[0].rows; + if (rows[0]) { + list = []; + l = rows[0].cells.length; + for (i = 0; i < l; i++) { + // tons of thanks to AnthonyM1229 for working out the following selector (issue #74) to make this work in IE8! + h = $headers.filter(':not([colspan])[data-column="' + i + '"]:last,[colspan="1"][data-column="' + i + '"]:last'); + ch = c.headers[i]; + // get column parser + p = getParserById( ts.getData(h, ch, 'sorter') ); + // empty cells behaviour - keeping emptyToBottom for backwards compatibility. + c.empties[i] = ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' ); + // text strings behaviour in numerical sorts + c.strings[i] = ts.getData(h, ch, 'string') || c.stringTo || 'max'; + if (!p) { + p = detectParserForColumn(table, rows, -1, i); + } + if (c.debug) { + parsersDebug += "column:" + i + "; parser:" + p.id + "; string:" + c.strings[i] + '; empty: ' + c.empties[i] + "\n"; + } + list.push(p); + } + } + if (c.debug) { + log(parsersDebug); + } + return list; + } + + /* utils */ + function buildCache(table) { + var b = table.tBodies, + tc = table.config, + totalRows, + totalCells, + parsers = tc.parsers, + t, i, j, k, c, cols, cacheTime; + tc.cache = {}; + if (tc.debug) { + cacheTime = new Date(); + } + for (k = 0; k < b.length; k++) { + tc.cache[k] = { row: [], normalized: [] }; + // ignore tbodies with class name from css.cssInfoBlock + if (!$(b[k]).hasClass(tc.cssInfoBlock)) { + $(b[k]).addClass('tablesorter-hidden'); + totalRows = (b[k] && b[k].rows.length) || 0; + totalCells = (b[k].rows[0] && b[k].rows[0].cells.length) || 0; + for (i = 0; i < totalRows; ++i) { + /** Add the table data to main data array */ + c = $(b[k].rows[i]); + cols = []; + // if this is a child row, add it to the last row's children and continue to the next row + if (c.hasClass(tc.cssChildRow)) { + tc.cache[k].row[tc.cache[k].row.length - 1] = tc.cache[k].row[tc.cache[k].row.length - 1].add(c); + // go to the next for loop + continue; + } + tc.cache[k].row.push(c); + for (j = 0; j < totalCells; ++j) { + t = getElementText(table, c[0].cells[j], j); + // allow parsing if the string is empty, previously parsing would change it to zero, + // in case the parser needs to extract data from the table cell attributes + cols.push( parsers[j].format(t, table, c[0].cells[j], j) ); + } + cols.push(tc.cache[k].normalized.length); // add position for rowCache + tc.cache[k].normalized.push(cols); + } + $(b[k]).removeClass('tablesorter-hidden'); + } + } + if (tc.debug) { + benchmark("Building cache for " + totalRows + " rows", cacheTime); + } + } + + function getWidgetById(name) { + var i, w, l = widgets.length; + for (i = 0; i < l; i++) { + w = widgets[i]; + if (w && w.hasOwnProperty('id') && w.id.toLowerCase() === name.toLowerCase()) { + return w; + } + } + } + + function applyWidget(table, init) { + var tc = table.config, c = tc.widgets, + time, i, w, l = c.length; + if (tc.debug) { + time = new Date(); + } + for (i = 0; i < l; i++) { + w = getWidgetById(c[i]); + if ( w ) { + if (init === true && w.hasOwnProperty('init')) { + w.init(table, widgets, w); + } else if (!init && w.hasOwnProperty('format')) { + w.format(table); + } + } + } + if (tc.debug) { + benchmark("Completed " + (init === true ? "initializing" : "applying") + " widgets", time); + } + } + + // init flag (true) used by pager plugin to prevent widget application + function appendToTable(table, init) { + var c = table.config, + b = table.tBodies, + rows = [], + r, n, totalRows, checkCell, c2 = c.cache, + f, i, j, k, l, pos, appendTime; + if (c.debug) { + appendTime = new Date(); + } + for (k = 0; k < b.length; k++) { + if (!$(b[k]).hasClass(c.cssInfoBlock)){ + $(b[k]).addClass('tablesorter-hidden'); + f = document.createDocumentFragment(); + r = c2[k].row; + n = c2[k].normalized; + totalRows = n.length; + checkCell = totalRows ? (n[0].length - 1) : 0; + for (i = 0; i < totalRows; i++) { + pos = n[i][checkCell]; + rows.push(r[pos]); + // removeRows used by the pager plugin + if (!c.appender || !c.removeRows) { + l = r[pos].length; + for (j = 0; j < l; j++) { + f.appendChild(r[pos][j]); + } + } + } + table.tBodies[k].appendChild(f); + $(b[k]).removeClass('tablesorter-hidden'); + } + } + if (c.appender) { + c.appender(table, rows); + } + if (c.debug) { + benchmark("Rebuilt table", appendTime); + } + // apply table widgets + if (!init) { applyWidget(table); } + // trigger sortend + $(table).trigger("sortEnd", table); + } + + // computeTableHeaderCellIndexes from: + // http://www.javascripttoolbox.com/lib/table/examples.php + // http://www.javascripttoolbox.com/temp/table_cellindex.html + function computeThIndexes(t) { + var matrix = [], + lookup = {}, + trs = $(t).find('thead:eq(0) tr'), + i, j, k, l, c, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol, matrixrow; + for (i = 0; i < trs.length; i++) { + cells = trs[i].cells; + for (j = 0; j < cells.length; j++) { + c = cells[j]; + rowIndex = c.parentNode.rowIndex; + cellId = rowIndex + "-" + c.cellIndex; + rowSpan = c.rowSpan || 1; + colSpan = c.colSpan || 1; + if (typeof(matrix[rowIndex]) === "undefined") { + matrix[rowIndex] = []; + } + // Find first available column in the first row + for (k = 0; k < matrix[rowIndex].length + 1; k++) { + if (typeof(matrix[rowIndex][k]) === "undefined") { + firstAvailCol = k; + break; + } + } + lookup[cellId] = firstAvailCol; + // add data-column + $(c).attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex + for (k = rowIndex; k < rowIndex + rowSpan; k++) { + if (typeof(matrix[k]) === "undefined") { + matrix[k] = []; + } + matrixrow = matrix[k]; + for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) { + matrixrow[l] = "x"; + } + } + } + } + return lookup; + } + + function formatSortingOrder(v) { + // look for "d" in "desc" order; return true + return (/^d/i.test(v) || v === 1); + } + + + function buildHeaders(table) { + var header_index = computeThIndexes(table), ch, $t, + $th, lock, time, $tableHeaders, c = table.config, ts = $.tablesorter; + c.headerList = []; + if (c.debug) { + time = new Date(); + } + $tableHeaders = $(table).find(c.selectorHeaders) + .each(function(index) { + $t = $(this); + ch = c.headers[index]; + this.innerHTML = '
    ' + this.innerHTML + '
    '; // faster than wrapInner + if (c.onRenderHeader) { c.onRenderHeader.apply($t, [index]); } + this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; + this.order = formatSortingOrder( ts.getData($t, ch, 'sortInitialOrder') || c.sortInitialOrder ) ? [1,0,2] : [0,1,2]; + this.count = -1; // set to -1 because clicking on the header automatically adds one + if (ts.getData($t, ch, 'sorter') === 'false') { this.sortDisabled = true; } + this.lockedOrder = false; + lock = ts.getData($t, ch, 'lockedOrder') || false; + if (typeof(lock) !== 'undefined' && lock !== false) { + this.order = this.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0]; + } + if (!this.sortDisabled) { + $th = $t.addClass(c.cssHeader); + } + // add cell to headerList + c.headerList[index] = this; + // add to parent in case there are multiple rows + $t.parent().addClass(c.cssHeader); + }); + if (table.config.debug) { + benchmark("Built headers:", time); + log($tableHeaders); + } + return $tableHeaders; + } + + function isValueInArray(v, a) { + var i, l = a.length; + for (i = 0; i < l; i++) { + if (a[i][0] === v) { + return true; + } + } + return false; + } + + function setHeadersCss(table, $headers, list) { + var f, h = [], i, j, l, css = [table.config.cssDesc, table.config.cssAsc]; + // remove all header information + $headers + .removeClass(css.join(' ')) + .each(function() { + if (!this.sortDisabled) { + h[this.column] = $(this); + } + }); + l = list.length; + for (i = 0; i < l; i++) { + if (list[i][1] === 2) { continue; } // direction = 2 means reset! + if (h[list[i][0]]) { + // add class if cell exists - fix for issue #78 + h[list[i][0]].addClass(css[list[i][1]]); + } + // multicolumn sorting updating + f = $headers.filter('[data-column="' + list[i][0] + '"]'); + if (l > 1 && f.length) { + for (j = 0; j < f.length; j++) { + if (!f[j].sortDisabled) { + $(f[j]).addClass(css[list[i][1]]); + } + } + } + } + } + + function fixColumnWidth(table) { + if (table.config.widthFixed) { + var colgroup = $('
    '); + $("tr:first td", table.tBodies[0]).each(function() { + colgroup.append($('').css('width', $(this).width())); + }); + $(table).prepend(colgroup); + } + } + + function updateHeaderSortCount(table, sortList) { + var i, s, o, c = table.config, + l = sortList.length; + for (i = 0; i < l; i++) { + s = sortList[i]; + o = c.headerList[s[0]]; + if (o) { // prevents error if sorton array is wrong + o.count = s[1] % (c.sortReset ? 3 : 2); + } + } + } + + function getCachedSortType(parsers, i) { + return (parsers && parsers[i]) ? parsers[i].type || '' : ''; + } + + /* sorting methods - reverted sorting method back to version 2.0.3 */ + function multisort(table, sortList) { + var dynamicExp, sortWrapper, col, mx = 0, dir = 0, tc = table.config, + l = sortList.length, bl = table.tBodies.length, + sortTime, i, j, k, c, cache, lc, s, e, order, orgOrderCol; + if (tc.debug) { sortTime = new Date(); } + for (k = 0; k < bl; k++) { + dynamicExp = "sortWrapper = function(a,b) {"; + cache = tc.cache[k]; + lc = cache.normalized.length; + for (i = 0; i < l; i++) { + c = sortList[i][0]; + order = sortList[i][1]; + // fallback to natural sort since it is more robust + s = /n/i.test(getCachedSortType(tc.parsers, c)) ? "Numeric" : "Text"; + s += order === 0 ? "" : "Desc"; + e = "e" + i; + // get max column value (ignore sign) + if (/Numeric/.test(s) && tc.strings[c]) { + for (j = 0; j < lc; j++) { + col = Math.abs(parseFloat(cache.normalized[j][c])); + mx = Math.max( mx, isNaN(col) ? 0 : col ); + } + // sort strings in numerical columns + if (typeof(tc.string[tc.strings[c]]) === 'boolean') { + dir = (order === 0 ? 1 : -1) * (tc.string[tc.strings[c]] ? -1 : 1); + } else { + dir = (tc.strings[c]) ? tc.string[tc.strings[c]] || 0 : 0; + } + } + dynamicExp += "var " + e + " = $.tablesorter.sort" + s + "(table,a[" + c + "],b[" + c + "]," + c + "," + mx + "," + dir + "); "; + dynamicExp += "if (" + e + ") { return " + e + "; } "; + dynamicExp += "else { "; + } + // if value is the same keep orignal order + orgOrderCol = (cache.normalized && cache.normalized[0]) ? cache.normalized[0].length - 1 : 0; + dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];"; + for (i=0; i < l; i++) { + dynamicExp += "}; "; + } + dynamicExp += "return 0; "; + dynamicExp += "}; "; + cache.normalized.sort(eval(dynamicExp)); // sort using eval expression + } + if (tc.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order+ " time", sortTime); } + } + + function checkResort($table, flag, callback) { + var t = $table[0]; + if (flag !== false) { + $table.trigger("sorton", [t.config.sortList, function(){ + $table.trigger('updateComplete'); + if (typeof callback === "function") { + callback(t); + } + }]); + } else { + $table.trigger('updateComplete'); + if (typeof callback === "function") { + callback(t); + } + } + } + + /* public methods */ + this.construct = function(settings) { + return this.each(function() { + // if no thead or tbody quit. + if (!this.tHead || this.tBodies.length === 0) { return; } + // declare + var $headers, $cell, $this, + c, i, j, k, a, s, o, downTime, + m = $.metadata; + // new blank config object + this.config = {}; + // merge and extend. + c = $.extend(true, this.config, $.tablesorter.defaults, settings); + + if (c.debug) { $.data( this, 'startoveralltimer', new Date()); } + // store common expression for speed + $this = $(this).addClass(c.tableClass); + // save the settings where they read + $.data(this, "tablesorter", c); + c.supportsTextContent = $('x')[0].textContent === 'x'; + // digit sort text location; keeping max+/- for backwards compatibility + c.string = { 'max': 1, 'min': -1, 'max+': 1, 'max-': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false }; + // build headers + $headers = buildHeaders(this); + // try to auto detect column type, and store in tables config + c.parsers = buildParserCache(this, $headers); + // build the cache for the tbody cells + // delayInit will delay building the cache until the user starts a sort + if (!c.delayInit) { buildCache(this); } + // fixate columns if the users supplies the fixedWidth option + fixColumnWidth(this); + // apply event handling to headers + // this is to big, perhaps break it out? + $headers.bind('mousedown.tablesorter mouseup.tablesorter', function(e, external) { + if (e.type === 'mousedown') { + downTime = new Date().getTime(); + return !c.cancelSelection; + } + // prevent resizable widget from initializing a sort (long clicks are ignored) + if (external !== true && (new Date().getTime() - downTime > 500)) { return false; } + if (c.delayInit && !c.cache) { buildCache($this[0]); } + if (!this.sortDisabled) { + // Only call sortStart if sorting is enabled. + $this.trigger("sortStart", $this[0]); + // store exp, for speed + $cell = $(this); + k = !e[c.sortMultiSortKey]; + // get current column sort order + this.count = (this.count + 1) % (c.sortReset ? 3 : 2); + // reset all sorts on non-current column - issue #30 + if (c.sortRestart) { + i = this; + $headers.each(function() { + // only reset counts on columns that weren't just clicked on and if not included in a multisort + if (this !== i && (k || !$(this).is('.' + c.cssDesc + ',.' + c.cssAsc))) { + this.count = -1; + } + }); + } + // get current column index + i = this.column; + // user only wants to sort on one column + if (k) { + // flush the sort list + c.sortList = []; + if (c.sortForce !== null) { + a = c.sortForce; + for (j = 0; j < a.length; j++) { + if (a[j][0] !== i) { + c.sortList.push(a[j]); + } + } + } + // add column to sort list + o = this.order[this.count]; + if (o < 2) { + c.sortList.push([i, o]); + // add other columns if header spans across multiple + if (this.colSpan > 1) { + for (j = 1; j < this.colSpan; j++) { + c.sortList.push([i + j, o]); + } + } + } + // multi column sorting + } else { + // the user has clicked on an already sorted column. + if (isValueInArray(i, c.sortList)) { + // reverse the sorting direction for all tables. + for (j = 0; j < c.sortList.length; j++) { + s = c.sortList[j]; + o = c.headerList[s[0]]; + if (s[0] === i) { + s[1] = o.order[o.count]; + if (s[1] === 2) { + c.sortList.splice(j,1); + o.count = -1; + } + } + } + } else { + // add column to sort list array + o = this.order[this.count]; + if (o < 2) { + c.sortList.push([i, o]); + // add other columns if header spans across multiple + if (this.colSpan > 1) { + for (j = 1; j < this.colSpan; j++) { + c.sortList.push([i + j, o]); + } + } + } + } + } + if (c.sortAppend !== null) { + a = c.sortAppend; + for (j = 0; j < a.length; j++) { + if (a[j][0] !== i) { + c.sortList.push(a[j]); + } + } + } + // sortBegin event triggered immediately before the sort + $this.trigger("sortBegin", $this[0]); + // set css for headers + setHeadersCss($this[0], $headers, c.sortList); + multisort($this[0], c.sortList); + appendToTable($this[0]); + } + }); + if (c.cancelSelection) { + // cancel selection + $headers.each(function() { + this.onselectstart = function() { + return false; + }; + }); + } + // apply easy methods that trigger binded events + $this + .bind("update", function(e, resort, callback) { + // remove rows/elements before update + $(c.selectorRemove, this).remove(); + // rebuild parsers. + c.parsers = buildParserCache(this, $headers); + // rebuild the cache map + buildCache(this); + checkResort($this, resort, callback); + }) + .bind("updateCell", function(e, cell, resort, callback) { + // get position from the dom. + var t = this, $tb = $(this).find('tbody'), row, pos, + // update cache - format: function(s, table, cell, cellIndex) + tbdy = $tb.index( $(cell).closest('tbody') ); + row = $tb.eq(tbdy).find('tr').index( $(cell).closest('tr') ); + pos = [ row, cell.cellIndex]; + t.config.cache[tbdy].normalized[pos[0]][pos[1]] = c.parsers[pos[1]].format( getElementText(t, cell, pos[1]), t, cell, pos[1] ); + checkResort($this, resort, callback); + }) + .bind("addRows", function(e, $row, resort, callback) { + var i, rows = $row.filter('tr').length, + dat = [], l = $row[0].cells.length, t = this, + tbdy = $(this).find('tbody').index( $row.closest('tbody') ); + // add each row + for (i = 0; i < rows; i++) { + // add each cell + for (j = 0; j < l; j++) { + dat[j] = c.parsers[j].format( getElementText(t, $row[i].cells[j], j), t, $row[i].cells[j], j ); + } + // add the row index to the end + dat.push(c.cache[tbdy].row.length); + // update cache + c.cache[tbdy].row.push([$row[i]]); + c.cache[tbdy].normalized.push(dat); + dat = []; + } + // resort using current settings + checkResort($this, resort, callback); + }) + .bind("sorton", function(e, list, callback, init) { + $(this).trigger("sortStart", this); + var l = c.headerList.length; + c.sortList = []; + $.each(list, function(i,v){ + // make sure column exists + if (v[0] < l) { c.sortList.push(list[i]); } + }); + // update header count index + updateHeaderSortCount(this, c.sortList); + // set css for headers + setHeadersCss(this, $headers, c.sortList); + // sort the table and append it to the dom + multisort(this, c.sortList); + appendToTable(this, init); + if (typeof callback === "function") { + callback(this); + } + }) + .bind("appendCache", function(e, init) { + appendToTable(this, init); + }) + .bind("applyWidgetId", function(e, id) { + getWidgetById(id).format(this); + }) + .bind("applyWidgets", function(e, init) { + // apply widgets + applyWidget(this, init); + }) + .bind("destroy", function(e,c){ + $.tablesorter.destroy(this, c); + }); + + // get sort list from jQuery data or metadata + if ($this.data() && typeof $this.data().sortlist !== 'undefined') { + c.sortList = $this.data().sortlist; + } else if (m && ($this.metadata() && $this.metadata().sortlist)) { + c.sortList = $this.metadata().sortlist; + } + // apply widget init code + applyWidget(this, true); + // if user has supplied a sort list to constructor. + if (c.sortList.length > 0) { + $this.trigger("sorton", [c.sortList, {}, !c.initWidgets]); + } else if (c.initWidgets) { + // apply widget format + applyWidget(this); + } + + // initialized + this.hasInitialized = true; + if (c.debug) { + $.tablesorter.benchmark("Overall initialization time", $.data( this, 'startoveralltimer')); + } + $this.trigger('tablesorter-initialized', this); + if (typeof c.initialized === 'function') { c.initialized(this); } + }); + }; + + // Natural sort - https://github.com/overset/javascript-natural-sort + this.sortText = function(table, a, b, col) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ], + r = $.tablesorter.regex, xN, xD, yN, yD, xF, yF, i, mx; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } + if (typeof c.textSorter === 'function') { return c.textSorter(a, b, table, col); } + // chunk/tokenize + xN = a.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); + yN = b.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); + // numeric, hex or date detection + xD = parseInt(a.match(r[2]),16) || (xN.length !== 1 && a.match(r[1]) && Date.parse(a)); + yD = parseInt(b.match(r[2]),16) || (xD && b.match(r[1]) && Date.parse(b)) || null; + // first try and sort Hex codes or Dates + if (yD) { + if ( xD < yD ) { return -1; } + if ( xD > yD ) { return 1; } + } + mx = Math.max(xN.length, yN.length); + // natural sorting through split numeric strings and default strings + for (i = 0; i < mx; i++) { + // find floats not starting with '0', string or 0 if not defined (Clint Priest) + xF = (!(xN[i] || '').match(r[3]) && parseFloat(xN[i])) || xN[i] || 0; + yF = (!(yN[i] || '').match(r[3]) && parseFloat(yN[i])) || yN[i] || 0; + // handle numeric vs string comparison - number < string - (Kyle Adams) + if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; } + // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' + if (typeof xF !== typeof yF) { + xF += ''; + yF += ''; + } + if (xF < yF) { return -1; } + if (xF > yF) { return 1; } + } + return 0; + }; + + this.sortTextDesc = function(table, a, b, col) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } + if (typeof c.textSorter === 'function') { return c.textSorter(b, a, table, col); } + return this.sortText(table, b, a); + }; + + // return text string value by adding up ascii value + // so the text is somewhat sorted when using a digital sort + // this is NOT an alphanumeric sort + this.getTextValue = function(a, mx, d) { + if (mx) { + // make sure the text value is greater than the max numerical value (mx) + var i, l = a.length, n = mx + d; + for (i = 0; i < l; i++) { + n += a.charCodeAt(i); + } + return d * n; + } + return 0; + }; + + this.sortNumeric = function(table, a, b, col, mx, d) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } + if (isNaN(a)) { a = this.getTextValue(a, mx, d); } + if (isNaN(b)) { b = this.getTextValue(b, mx, d); } + return a - b; + }; + + this.sortNumericDesc = function(table, a, b, col, mx, d) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } + if (isNaN(a)) { a = this.getTextValue(a, mx, d); } + if (isNaN(b)) { b = this.getTextValue(b, mx, d); } + return b - a; + }; + + this.destroy = function(table, removeClasses){ + var $t = $(table), c = table.config; + // remove widget added rows + $t.find('thead:first tr:not(.' + c.cssHeader + ')').remove(); + // remove resizer widget stuff + $t.find('thead:first .tablesorter-resizer').remove(); + // disable tablesorter + $t + .unbind('update updateCell addRows sorton appendCache applyWidgetId applyWidgets destroy mouseup mouseleave') + .find(c.selectorHeaders) + .unbind('click mousedown mousemove mouseup') + .removeClass(c.cssHeader + ' ' + c.cssAsc + ' ' + c.cssDesc); + if (removeClasses !== false) { + $t.removeClass(c.tableClass); + } + }; + + this.addParser = function(parser) { + var i, l = parsers.length, a = true; + for (i = 0; i < l; i++) { + if (parsers[i].id.toLowerCase() === parser.id.toLowerCase()) { + a = false; + } + } + if (a) { + parsers.push(parser); + } + }; + this.addWidget = function(widget) { + widgets.push(widget); + }; + + this.formatFloat = function(s, table) { + if (typeof(s) !== 'string' || s === '') { return s; } + if (table.config.usNumberFormat !== false) { + // US Format - 1,234,567.89 -> 1234567.89 + s = s.replace(/,/g,''); + } else { + // German Format = 1.234.567,89 -> 1234567.89 + // French Format = 1 234 567,89 -> 1234567.89 + s = s.replace(/[\s|\.]/g,'').replace(/,/g,'.'); + } + if(/^\s*\([.\d]+\)/.test(s)) { + s = s.replace(/^\s*\(/,'-').replace(/\)/,''); + } + var i = parseFloat(s); + // return the text instead of zero + return isNaN(i) ? $.trim(s) : i; + }; + this.isDigit = function(s) { + // replace all unwanted chars and match. + return (/^[\-+(]?\d+[)]?$/).test(s.replace(/[,.'\s]/g, '')); + }; + + // regex used in natural sort + this.regex = [ + /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi, // chunk/tokenize numbers & letters + /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/, //date + /^0x[0-9a-f]+$/i, // hex + /^0/ // leading zeros + ]; + // used when replacing accented characters during sorting + this.characterEquivalents = { + "a" : "\u00e1\u00e0\u00e2\u00e3\u00e4", // áàâãä + "A" : "\u00c1\u00c0\u00c2\u00c3\u00c4", // ÁÀÂÃÄ + "c" : "\u00e7", // ç + "C" : "\u00c7", // Ç + "e" : "\u00e9\u00e8\u00ea\u00eb", // éèêë + "E" : "\u00c9\u00c8\u00ca\u00cb", // ÉÈÊË + "i" : "\u00ed\u00ec\u0130\u00ee\u00ef", // íìİîï + "I" : "\u00cd\u00cc\u0130\u00ce\u00cf", // ÍÌİÎÏ + "o" : "\u00f3\u00f2\u00f4\u00f5\u00f6", // óòôõö + "O" : "\u00d3\u00d2\u00d4\u00d5\u00d6", // ÓÒÔÕÖ + "S" : "\u00df", // ß + "u" : "\u00fa\u00f9\u00fb\u00fc", // úùûü + "U" : "\u00da\u00d9\u00db\u00dc" // ÚÙÛÜ + }; + this.replaceAccents = function(s) { + var a, acc = '[', eq = this.characterEquivalents; + if (!this.characterRegex) { + this.characterRegexArray = {}; + for (a in eq) { + if (typeof a === 'string') { + acc += eq[a]; + this.characterRegexArray[a] = new RegExp('[' + eq[a] + ']', 'g'); + } + } + this.characterRegex = new RegExp(acc + ']'); + } + if (this.characterRegex.test(s)) { + for (a in eq) { + if (typeof a === 'string') { + s = s.replace( this.characterRegexArray[a], a ); + } + } + } + return s; + }; + + // get sorter, string, empty, etc options for each column from + // jQuery data, metadata, header option or header class name ("sorter-false") + // priority = jQuery data > meta > headers option > header class name + this.getData = function(h, ch, key) { + var val = '', $h = $(h), m, cl; + if (!$h.length) { return ''; } + m = $.metadata ? $h.metadata() : false; + cl = ' ' + ($h.attr('class') || ''); + if ($h.data() && ( typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined') ){ + // "data-lockedOrder" is assigned to "lockedorder"; but "data-locked-order" is assigned to "lockedOrder" + // "data-sort-initial-order" is assigned to "sortInitialOrder" + val += $h.data(key) || $h.data(key.toLowerCase()); + } else if (m && typeof m[key] !== 'undefined') { + val += m[key]; + } else if (ch && typeof ch[key] !== 'undefined') { + val += ch[key]; + } else if (cl && cl.match(' ' + key + '-')) { + // include sorter class name "sorter-text", etc + val = cl.match( new RegExp(' ' + key + '-(\\w+)') )[1] || ''; + } + return $.trim(val); + }; + + this.clearTableBody = function(table) { + $(table.tBodies).filter(':not(.' + table.config.cssInfoBlock + ')').empty(); + }; + + } + })(); + + // make shortcut + var ts = $.tablesorter; + + // extend plugin scope + $.fn.extend({ + tablesorter: ts.construct + }); + + // add default parsers + ts.addParser({ + id: "text", + is: function(s, table, node) { + return true; + }, + format: function(s, table, cell, cellIndex) { + var c = table.config; + s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s ); + return c.sortLocaleCompare ? ts.replaceAccents(s) : s; + }, + type: "text" + }); + + ts.addParser({ + id: "currency", + is: function(s) { + return (/^\(?[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+/).test(s); // £$€¤¥¢ + }, + format: function(s, table) { + return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "ipAddress", + is: function(s) { + return (/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/).test(s); + }, + format: function(s, table) { + var i, a = s.split("."), + r = "", + l = a.length; + for (i = 0; i < l; i++) { + r += ("00" + a[i]).slice(-3); + } + return ts.formatFloat(r, table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "url", + is: function(s) { + return (/^(https?|ftp|file):\/\//).test(s); + }, + format: function(s) { + return $.trim(s.replace(/(https?|ftp|file):\/\//, '')); + }, + type: "text" + }); + + ts.addParser({ + id: "isoDate", + is: function(s) { + return (/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/).test(s); + }, + format: function(s, table) { + return ts.formatFloat((s !== "") ? (new Date(s.replace(/-/g, "/")).getTime() || "") : "", table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "percent", + is: function(s) { + return (/\d%\)?$/).test(s); + }, + format: function(s, table) { + return ts.formatFloat(s.replace(/%/g, ""), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "usLongDate", + is: function(s) { + return s.match(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/); + }, + format: function(s, table) { + return ts.formatFloat( (new Date(s).getTime() || ''), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "shortDate", // "mmddyyyy", "ddmmyyyy" or "yyyymmdd" + is: function(s) { + // testing for ####-##-#### - so it's not perfect + return (/^(\d{2}|\d{4})[\/\-\,\.\s+]\d{2}[\/\-\.\,\s+](\d{2}|\d{4})$/).test(s); + }, + format: function(s, table, cell, cellIndex) { + var c = table.config, ci = c.headerList[cellIndex], + format = ci.shortDateFormat; + if (typeof format === 'undefined') { + // cache header formatting so it doesn't getData for every cell in the column + format = ci.shortDateFormat = ts.getData( ci, c.headers[cellIndex], 'dateFormat') || c.dateFormat; + } + s = s.replace(/\s+/g," ").replace(/[\-|\.|\,]/g, "/"); + if (format === "mmddyyyy") { + s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$1/$2"); + } else if (format === "ddmmyyyy") { + s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$2/$1"); + } else if (format === "yyyymmdd") { + s = s.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/, "$1/$2/$3"); + } + return ts.formatFloat( (new Date(s).getTime() || ''), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "time", + is: function(s) { + return (/^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/).test(s); + }, + format: function(s, table) { + return ts.formatFloat( (new Date("2000/01/01 " + s).getTime() || ''), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "digit", + is: function(s) { + return ts.isDigit(s); + }, + format: function(s, table) { + return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "metadata", + is: function(s) { + return false; + }, + format: function(s, table, cell) { + var c = table.config, + p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; + return $(cell).metadata()[p]; + }, + type: "numeric" + }); + + // add default widgets + ts.addWidget({ + id: "zebra", + format: function(table) { + var $tb, $tv, $tr, row, even, time, k, l, + c = table.config, + child = new RegExp(c.cssChildRow, 'i'), + b = $(table).children('tbody:not(.' + c.cssInfoBlock + ')'), + css = [ "even", "odd" ]; + // maintain backwards compatibility + css = c.widgetZebra && c.hasOwnProperty('css') ? c.widgetZebra.css : + (c.widgetOptions && c.widgetOptions.hasOwnProperty('zebra')) ? c.widgetOptions.zebra : css; + if (c.debug) { + time = new Date(); + } + for (k = 0; k < b.length; k++ ) { + // loop through the visible rows + $tb = $(b[k]); + l = $tb.children('tr').length; + if (l > 1) { + row = 0; + $tv = $tb.find('tr:visible'); + $tb.addClass('tablesorter-hidden'); + // revered back to using jQuery each - strangely it's the fastest method + $tv.each(function(){ + $tr = $(this); + // style children rows the same way the parent row was styled + if (!child.test(this.className)) { row++; } + even = (row % 2 === 0); + $tr.removeClass(css[even ? 1 : 0]).addClass(css[even ? 0 : 1]); + }); + $tb.removeClass('tablesorter-hidden'); + } + } + if (c.debug) { + ts.benchmark("Applying Zebra widget", time); + } + } + }); + +})(jQuery); \ No newline at end of file diff --git a/sitestatic/jquery.tablesorter-2.3.11.min.js b/sitestatic/jquery.tablesorter-2.3.11.min.js new file mode 100644 index 00000000..20f808fa --- /dev/null +++ b/sitestatic/jquery.tablesorter-2.3.11.min.js @@ -0,0 +1,6 @@ +/*! +* TableSorter 2.3.11 - Client-side table sorting with ease! +* Minified using UglifyJS (http://jscompress.com/) +* Copyright (c) 2007 Christian Bach +*/ +!function($){$.extend({tablesorter:new function(){function log(a){if(typeof console!=="undefined"&&typeof console.log!=="undefined"){console.log(a)}else{alert(a)}}function benchmark(a,b){log(a+" ("+((new Date).getTime()-b.getTime())+"ms)")}function getElementText(a,b,c){if(!b){return""}var d=a.config,e=d.textExtraction,f="";if(e==="simple"){if(d.supportsTextContent){f=b.textContent}else{f=$(b).text()}}else{if(typeof e==="function"){f=e(b,a,c)}else if(typeof e==="object"&&e.hasOwnProperty(c)){f=e[c](b,a,c)}else{f=d.supportsTextContent?b.textContent:$(b).text()}}return $.trim(f)}function getParserById(a){var b,c=parsers.length;for(b=0;b'+this.innerHTML+"";if(i.onRenderHeader){i.onRenderHeader.apply(d,[a])}this.column=b[this.parentNode.rowIndex+"-"+this.cellIndex];this.order=formatSortingOrder(j.getData(d,c,"sortInitialOrder")||i.sortInitialOrder)?[1,0,2]:[0,1,2];this.count=-1;if(j.getData(d,c,"sorter")==="false"){this.sortDisabled=true}this.lockedOrder=false;f=j.getData(d,c,"lockedOrder")||false;if(typeof f!=="undefined"&&f!==false){this.order=this.lockedOrder=formatSortingOrder(f)?[1,1,1]:[0,0,0]}if(!this.sortDisabled){e=d.addClass(i.cssHeader)}i.headerList[a]=this;d.parent().addClass(i.cssHeader)});if(a.config.debug){benchmark("Built headers:",g);log(h)}return h}function isValueInArray(a,b){var c,d=b.length;for(c=0;c1&&d.length){for(g=0;g");$("tr:first td",a.tBodies[0]).each(function(){b.append($("").css("width",$(this).width()))});$(a).prepend(b)}}function updateHeaderSortCount(a,b){var c,d,e,f=a.config,g=b.length;for(c=0;c thead th",selectorRemove:"tr.remove-me",debug:false,headerList:[],empties:{},strings:{},parsers:[]};this.benchmark=benchmark;this.hasInitialized=false;this.construct=function(a){return this.each(function(){if(!this.tHead||this.tBodies.length===0){return}var b,c,d,e,f,g,h,i,j,k,l,m=$.metadata;this.config={};e=$.extend(true,this.config,$.tablesorter.defaults,a);if(e.debug){$.data(this,"startoveralltimer",new Date)}d=$(this).addClass(e.tableClass);$.data(this,"tablesorter",e);e.supportsTextContent=$("x")[0].textContent==="x";e.string={max:1,min:-1,"max+":1,"max-":-1,zero:0,none:0,"null":0,top:true,bottom:false};b=buildHeaders(this);e.parsers=buildParserCache(this,b);if(!e.delayInit){buildCache(this)}fixColumnWidth(this);b.bind("mousedown.tablesorter mouseup.tablesorter",function(a,m){if(a.type==="mousedown"){l=(new Date).getTime();return!e.cancelSelection}if(m!==true&&(new Date).getTime()-l>500){return false}if(e.delayInit&&!e.cache){buildCache(d[0])}if(!this.sortDisabled){d.trigger("sortStart",d[0]);c=$(this);h=!a[e.sortMultiSortKey];this.count=(this.count+1)%(e.sortReset?3:2);if(e.sortRestart){f=this;b.each(function(){if(this!==f&&(h||!$(this).is("."+e.cssDesc+",."+e.cssAsc))){this.count=-1}})}f=this.column;if(h){e.sortList=[];if(e.sortForce!==null){i=e.sortForce;for(g=0;g1){for(g=1;g1){for(g=1;g0){d.trigger("sorton",[e.sortList,{},!e.initWidgets])}else if(e.initWidgets){applyWidget(this)}this.hasInitialized=true;if(e.debug){$.tablesorter.benchmark("Overall initialization time",$.data(this,"startoveralltimer"))}d.trigger("tablesorter-initialized",this);if(typeof e.initialized==="function"){e.initialized(this)}})};this.sortText=function(a,b,c,d){if(b===c){return 0}var e=a.config,f=e.string[e.empties[d]||e.emptyTo],g=$.tablesorter.regex,h,i,j,k,l,m,n,o;if(b===""&&f!==0){return typeof f==="boolean"?f?-1:1:-f||-1}if(c===""&&f!==0){return typeof f==="boolean"?f?1:-1:f||1}if(typeof e.textSorter==="function"){return e.textSorter(b,c,a,d)}h=b.replace(g[0],"\\0$1\\0").replace(/\\0$/,"").replace(/^\\0/,"").split("\\0");j=c.replace(g[0],"\\0$1\\0").replace(/\\0$/,"").replace(/^\\0/,"").split("\\0");i=parseInt(b.match(g[2]),16)||h.length!==1&&b.match(g[1])&&Date.parse(b);k=parseInt(c.match(g[2]),16)||i&&c.match(g[1])&&Date.parse(c)||null;if(k){if(ik){return 1}}o=Math.max(h.length,j.length);for(n=0;nm){return 1}}return 0};this.sortTextDesc=function(a,b,c,d){if(b===c){return 0}var e=a.config,f=e.string[e.empties[d]||e.emptyTo];if(b===""&&f!==0){return typeof f==="boolean"?f?-1:1:f||1}if(c===""&&f!==0){return typeof f==="boolean"?f?1:-1:-f||-1}if(typeof e.textSorter==="function"){return e.textSorter(c,b,a,d)}return this.sortText(a,c,b)};this.getTextValue=function(a,b,c){if(b){var d,e=a.length,f=b+c;for(d=0;d1){e=0;c=b.find("tr:visible");b.addClass("tablesorter-hidden");c.each(function(){d=$(this);if(!k.test(this.className)){e++}f=e%2===0;d.removeClass(m[f?1:0]).addClass(m[f?0:1])});b.removeClass("tablesorter-hidden")}}if(j.debug){ts.benchmark("Applying Zebra widget",g)}}})}(jQuery) diff --git a/sitestatic/jquery.tablesorter.js b/sitestatic/jquery.tablesorter.js deleted file mode 100644 index 331b7617..00000000 --- a/sitestatic/jquery.tablesorter.js +++ /dev/null @@ -1,1031 +0,0 @@ -/* - * - * TableSorter 2.0 - Client-side table sorting with ease! - * Version 2.0.5b - * @requires jQuery v1.2.3 - * - * Copyright (c) 2007 Christian Bach - * Examples and docs at: http://tablesorter.com - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - */ -/** - * - * @description Create a sortable table with multi-column sorting capabilitys - * - * @example $('table').tablesorter(); - * @desc Create a simple tablesorter interface. - * - * @example $('table').tablesorter({ sortList:[[0,0],[1,0]] }); - * @desc Create a tablesorter interface and sort on the first and secound column column headers. - * - * @example $('table').tablesorter({ headers: { 0: { sorter: false}, 1: {sorter: false} } }); - * - * @desc Create a tablesorter interface and disableing the first and second column headers. - * - * - * @example $('table').tablesorter({ headers: { 0: {sorter:"integer"}, 1: {sorter:"currency"} } }); - * - * @desc Create a tablesorter interface and set a column parser for the first - * and second column. - * - * - * @param Object - * settings An object literal containing key/value pairs to provide - * optional settings. - * - * - * @option String cssHeader (optional) A string of the class name to be appended - * to sortable tr elements in the thead of the table. Default value: - * "header" - * - * @option String cssAsc (optional) A string of the class name to be appended to - * sortable tr elements in the thead on a ascending sort. Default value: - * "headerSortUp" - * - * @option String cssDesc (optional) A string of the class name to be appended - * to sortable tr elements in the thead on a descending sort. Default - * value: "headerSortDown" - * - * @option String sortInitialOrder (optional) A string of the inital sorting - * order can be asc or desc. Default value: "asc" - * - * @option String sortMultisortKey (optional) A string of the multi-column sort - * key. Default value: "shiftKey" - * - * @option String textExtraction (optional) A string of the text-extraction - * method to use. For complex html structures inside td cell set this - * option to "complex", on large tables the complex option can be slow. - * Default value: "simple" - * - * @option Object headers (optional) An array containing the forces sorting - * rules. This option let's you specify a default sorting rule. Default - * value: null - * - * @option Array sortList (optional) An array containing the forces sorting - * rules. This option let's you specify a default sorting rule. Default - * value: null - * - * @option Array sortForce (optional) An array containing forced sorting rules. - * This option let's you specify a default sorting rule, which is - * prepended to user-selected rules. Default value: null - * - * @option Boolean sortLocaleCompare (optional) Boolean flag indicating whatever - * to use String.localeCampare method or not. Default set to true. - * - * - * @option Array sortAppend (optional) An array containing forced sorting rules. - * This option let's you specify a default sorting rule, which is - * appended to user-selected rules. Default value: null - * - * @option Boolean widthFixed (optional) Boolean flag indicating if tablesorter - * should apply fixed widths to the table columns. This is usefull when - * using the pager companion plugin. This options requires the dimension - * jquery plugin. Default value: false - * - * @option Boolean cancelSelection (optional) Boolean flag indicating if - * tablesorter should cancel selection of the table headers text. - * Default value: true - * - * @option Boolean debug (optional) Boolean flag indicating if tablesorter - * should display debuging information usefull for development. - * - * @type jQuery - * - * @name tablesorter - * - * @cat Plugins/Tablesorter - * - * @author Christian Bach/christian.bach@polyester.se - */ - -(function ($) { - $.extend({ - tablesorter: new - function () { - - var parsers = [], - widgets = []; - - this.defaults = { - cssHeader: "header", - cssAsc: "headerSortUp", - cssDesc: "headerSortDown", - cssChildRow: "expand-child", - sortInitialOrder: "asc", - sortMultiSortKey: "shiftKey", - sortForce: null, - sortAppend: null, - sortLocaleCompare: true, - textExtraction: "simple", - parsers: {}, widgets: [], - widgetZebra: { - css: ["even", "odd"] - }, headers: {}, widthFixed: false, - cancelSelection: true, - sortList: [], - headerList: [], - dateFormat: "us", - decimal: '/\.|\,/g', - onRenderHeader: null, - selectorHeaders: 'thead th', - debug: false - }; - - /* debuging utils */ - - function benchmark(s, d) { - log(s + "," + (new Date().getTime() - d.getTime()) + "ms"); - } - - this.benchmark = benchmark; - - function log(s) { - if (typeof console != "undefined" && typeof console.debug != "undefined") { - console.log(s); - } else { - alert(s); - } - } - - /* parsers utils */ - - function buildParserCache(table, $headers) { - - if (table.config.debug) { - var parsersDebug = ""; - } - - if (table.tBodies.length == 0) return; // In the case of empty tables - var rows = table.tBodies[0].rows; - - if (rows[0]) { - - var list = [], - cells = rows[0].cells, - l = cells.length; - - for (var i = 0; i < l; i++) { - - var p = false; - - if ($.metadata && ($($headers[i]).metadata() && $($headers[i]).metadata().sorter)) { - - p = getParserById($($headers[i]).metadata().sorter); - - } else if ((table.config.headers[i] && table.config.headers[i].sorter)) { - - p = getParserById(table.config.headers[i].sorter); - } - if (!p) { - - p = detectParserForColumn(table, rows, -1, i); - } - - if (table.config.debug) { - parsersDebug += "column:" + i + " parser:" + p.id + "\n"; - } - - list.push(p); - } - } - - if (table.config.debug) { - log(parsersDebug); - } - - return list; - }; - - function detectParserForColumn(table, rows, rowIndex, cellIndex) { - var l = parsers.length, - node = false, - nodeValue = false, - keepLooking = true; - while (nodeValue == '' && keepLooking) { - rowIndex++; - if (rows[rowIndex]) { - node = getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex); - nodeValue = trimAndGetNodeText(table.config, node); - if (table.config.debug) { - log('Checking if value was empty on row:' + rowIndex); - } - } else { - keepLooking = false; - } - } - for (var i = 1; i < l; i++) { - if (parsers[i].is(nodeValue, table, node)) { - return parsers[i]; - } - } - // 0 is always the generic parser (text) - return parsers[0]; - } - - function getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex) { - return rows[rowIndex].cells[cellIndex]; - } - - function trimAndGetNodeText(config, node) { - return $.trim(getElementText(config, node)); - } - - function getParserById(name) { - var l = parsers.length; - for (var i = 0; i < l; i++) { - if (parsers[i].id.toLowerCase() == name.toLowerCase()) { - return parsers[i]; - } - } - return false; - } - - /* utils */ - - function buildCache(table) { - - if (table.config.debug) { - var cacheTime = new Date(); - } - - var totalRows = (table.tBodies[0] && table.tBodies[0].rows.length) || 0, - totalCells = (table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length) || 0, - parsers = table.config.parsers, - cache = { - row: [], - normalized: [] - }; - - for (var i = 0; i < totalRows; ++i) { - - /** Add the table data to main data array */ - var c = $(table.tBodies[0].rows[i]), - cols = []; - - // if this is a child row, add it to the last row's children and - // continue to the next row - if (c.hasClass(table.config.cssChildRow)) { - cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add(c); - // go to the next for loop - continue; - } - - cache.row.push(c); - - for (var j = 0; j < totalCells; ++j) { - cols.push(parsers[j].format(getElementText(table.config, c[0].cells[j]), table, c[0].cells[j])); - } - - cols.push(cache.normalized.length); // add position for rowCache - cache.normalized.push(cols); - cols = null; - }; - - if (table.config.debug) { - benchmark("Building cache for " + totalRows + " rows:", cacheTime); - } - - return cache; - }; - - function getElementText(config, node) { - - var text = ""; - - if (!node) return ""; - - if (!config.supportsTextContent) config.supportsTextContent = node.textContent || false; - - if (config.textExtraction == "simple") { - if (config.supportsTextContent) { - text = node.textContent; - } else { - if (node.childNodes[0] && node.childNodes[0].hasChildNodes()) { - text = node.childNodes[0].innerHTML; - } else { - text = node.innerHTML; - } - } - } else { - if (typeof(config.textExtraction) == "function") { - text = config.textExtraction(node); - } else { - text = $(node).text(); - } - } - return text; - } - - function appendToTable(table, cache) { - - if (table.config.debug) { - var appendTime = new Date() - } - - var c = cache, - r = c.row, - n = c.normalized, - totalRows = n.length, - checkCell = (n[0].length - 1), - tableBody = $(table.tBodies[0]), - rows = []; - - - for (var i = 0; i < totalRows; i++) { - var pos = n[i][checkCell]; - - rows.push(r[pos]); - - if (!table.config.appender) { - - //var o = ; - var l = r[pos].length; - for (var j = 0; j < l; j++) { - tableBody[0].appendChild(r[pos][j]); - } - - // - } - } - - - - if (table.config.appender) { - - table.config.appender(table, rows); - } - - rows = null; - - if (table.config.debug) { - benchmark("Rebuilt table:", appendTime); - } - - // apply table widgets - applyWidget(table); - - // trigger sortend - setTimeout(function () { - $(table).trigger("sortEnd"); - }, 0); - - }; - - function buildHeaders(table) { - - if (table.config.debug) { - var time = new Date(); - } - - var meta = ($.metadata) ? true : false; - - var header_index = computeTableHeaderCellIndexes(table); - - $tableHeaders = $(table.config.selectorHeaders, table).each(function (index) { - - this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; - // this.column = index; - this.order = formatSortingOrder(table.config.sortInitialOrder); - - - this.count = this.order; - - if (checkHeaderMetadata(this) || checkHeaderOptions(table, index)) this.sortDisabled = true; - if (checkHeaderOptionsSortingLocked(table, index)) this.order = this.lockedOrder = checkHeaderOptionsSortingLocked(table, index); - - if (!this.sortDisabled) { - var $th = $(this).addClass(table.config.cssHeader); - if (table.config.onRenderHeader) table.config.onRenderHeader.apply($th); - } - - // add cell to headerList - table.config.headerList[index] = this; - }); - - if (table.config.debug) { - benchmark("Built headers:", time); - log($tableHeaders); - } - - return $tableHeaders; - - }; - - // from: - // http://www.javascripttoolbox.com/lib/table/examples.php - // http://www.javascripttoolbox.com/temp/table_cellindex.html - - - function computeTableHeaderCellIndexes(t) { - var matrix = []; - var lookup = {}; - var thead = t.getElementsByTagName('THEAD')[0]; - var trs = thead.getElementsByTagName('TR'); - - for (var i = 0; i < trs.length; i++) { - var cells = trs[i].cells; - for (var j = 0; j < cells.length; j++) { - var c = cells[j]; - - var rowIndex = c.parentNode.rowIndex; - var cellId = rowIndex + "-" + c.cellIndex; - var rowSpan = c.rowSpan || 1; - var colSpan = c.colSpan || 1 - var firstAvailCol; - if (typeof(matrix[rowIndex]) == "undefined") { - matrix[rowIndex] = []; - } - // Find first available column in the first row - for (var k = 0; k < matrix[rowIndex].length + 1; k++) { - if (typeof(matrix[rowIndex][k]) == "undefined") { - firstAvailCol = k; - break; - } - } - lookup[cellId] = firstAvailCol; - for (var k = rowIndex; k < rowIndex + rowSpan; k++) { - if (typeof(matrix[k]) == "undefined") { - matrix[k] = []; - } - var matrixrow = matrix[k]; - for (var l = firstAvailCol; l < firstAvailCol + colSpan; l++) { - matrixrow[l] = "x"; - } - } - } - } - return lookup; - } - - function checkCellColSpan(table, rows, row) { - var arr = [], - r = table.tHead.rows, - c = r[row].cells; - - for (var i = 0; i < c.length; i++) { - var cell = c[i]; - - if (cell.colSpan > 1) { - arr = arr.concat(checkCellColSpan(table, headerArr, row++)); - } else { - if (table.tHead.length == 1 || (cell.rowSpan > 1 || !r[row + 1])) { - arr.push(cell); - } - // headerArr[row] = (i+row); - } - } - return arr; - }; - - function checkHeaderMetadata(cell) { - if (($.metadata) && ($(cell).metadata().sorter === false)) { - return true; - }; - return false; - } - - function checkHeaderOptions(table, i) { - if ((table.config.headers[i]) && (table.config.headers[i].sorter === false)) { - return true; - }; - return false; - } - - function checkHeaderOptionsSortingLocked(table, i) { - if ((table.config.headers[i]) && (table.config.headers[i].lockedOrder)) return table.config.headers[i].lockedOrder; - return false; - } - - function applyWidget(table) { - var c = table.config.widgets; - var l = c.length; - for (var i = 0; i < l; i++) { - - getWidgetById(c[i]).format(table); - } - - } - - function getWidgetById(name) { - var l = widgets.length; - for (var i = 0; i < l; i++) { - if (widgets[i].id.toLowerCase() == name.toLowerCase()) { - return widgets[i]; - } - } - }; - - function formatSortingOrder(v) { - if (typeof(v) != "Number") { - return (v.toLowerCase() == "desc") ? 1 : 0; - } else { - return (v == 1) ? 1 : 0; - } - } - - function isValueInArray(v, a) { - var l = a.length; - for (var i = 0; i < l; i++) { - if (a[i][0] == v) { - return true; - } - } - return false; - } - - function setHeadersCss(table, $headers, list, css) { - // remove all header information - $headers.removeClass(css[0]).removeClass(css[1]); - - var h = []; - $headers.each(function (offset) { - if (!this.sortDisabled) { - h[this.column] = $(this); - } - }); - - var l = list.length; - for (var i = 0; i < l; i++) { - h[list[i][0]].addClass(css[list[i][1]]); - } - } - - function fixColumnWidth(table, $headers) { - var c = table.config; - if (c.widthFixed) { - var colgroup = $(''); - $("tr:first td", table.tBodies[0]).each(function () { - colgroup.append($('').css('width', $(this).width())); - }); - $(table).prepend(colgroup); - }; - } - - function updateHeaderSortCount(table, sortList) { - var c = table.config, - l = sortList.length; - for (var i = 0; i < l; i++) { - var s = sortList[i], - o = c.headerList[s[0]]; - o.count = s[1]; - o.count++; - } - } - - /* sorting methods */ - - function multisort(table, sortList, cache) { - - if (table.config.debug) { - var sortTime = new Date(); - } - - var dynamicExp = "var sortWrapper = function(a,b) {", - l = sortList.length; - - // TODO: inline functions. - for (var i = 0; i < l; i++) { - - var c = sortList[i][0]; - var order = sortList[i][1]; - // var s = (getCachedSortType(table.config.parsers,c) == "text") ? - // ((order == 0) ? "sortText" : "sortTextDesc") : ((order == 0) ? - // "sortNumeric" : "sortNumericDesc"); - // var s = (table.config.parsers[c].type == "text") ? ((order == 0) - // ? makeSortText(c) : makeSortTextDesc(c)) : ((order == 0) ? - // makeSortNumeric(c) : makeSortNumericDesc(c)); - var s = (table.config.parsers[c].type == "text") ? ((order == 0) ? makeSortFunction("text", "asc", c) : makeSortFunction("text", "desc", c)) : ((order == 0) ? makeSortFunction("numeric", "asc", c) : makeSortFunction("numeric", "desc", c)); - var e = "e" + i; - - dynamicExp += "var " + e + " = " + s; // + "(a[" + c + "],b[" + c - // + "]); "; - dynamicExp += "if(" + e + ") { return " + e + "; } "; - dynamicExp += "else { "; - - } - - // if value is the same keep orignal order - var orgOrderCol = cache.normalized[0].length - 1; - dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];"; - - for (var i = 0; i < l; i++) { - dynamicExp += "}; "; - } - - dynamicExp += "return 0; "; - dynamicExp += "}; "; - - if (table.config.debug) { - benchmark("Evaling expression:" + dynamicExp, new Date()); - } - - eval(dynamicExp); - - cache.normalized.sort(sortWrapper); - - if (table.config.debug) { - benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time:", sortTime); - } - - return cache; - }; - - function makeSortFunction(type, direction, index) { - var a = "a[" + index + "]", - b = "b[" + index + "]"; - if (type == 'text' && direction == 'asc') { - return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + a + " < " + b + ") ? -1 : 1 )));"; - } else if (type == 'text' && direction == 'desc') { - return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + b + " < " + a + ") ? -1 : 1 )));"; - } else if (type == 'numeric' && direction == 'asc') { - return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + a + " - " + b + "));"; - } else if (type == 'numeric' && direction == 'desc') { - return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + b + " - " + a + "));"; - } - }; - - function makeSortText(i) { - return "((a[" + i + "] < b[" + i + "]) ? -1 : ((a[" + i + "] > b[" + i + "]) ? 1 : 0));"; - }; - - function makeSortTextDesc(i) { - return "((b[" + i + "] < a[" + i + "]) ? -1 : ((b[" + i + "] > a[" + i + "]) ? 1 : 0));"; - }; - - function makeSortNumeric(i) { - return "a[" + i + "]-b[" + i + "];"; - }; - - function makeSortNumericDesc(i) { - return "b[" + i + "]-a[" + i + "];"; - }; - - function sortText(a, b) { - if (table.config.sortLocaleCompare) return a.localeCompare(b); - return ((a < b) ? -1 : ((a > b) ? 1 : 0)); - }; - - function sortTextDesc(a, b) { - if (table.config.sortLocaleCompare) return b.localeCompare(a); - return ((b < a) ? -1 : ((b > a) ? 1 : 0)); - }; - - function sortNumeric(a, b) { - return a - b; - }; - - function sortNumericDesc(a, b) { - return b - a; - }; - - function getCachedSortType(parsers, i) { - return parsers[i].type; - }; /* public methods */ - this.construct = function (settings) { - return this.each(function () { - // if no thead or tbody quit. - if (!this.tHead || !this.tBodies) return; - // declare - var $this, $document, $headers, cache, config, shiftDown = 0, - sortOrder; - // new blank config object - this.config = {}; - // merge and extend. - config = $.extend(this.config, $.tablesorter.defaults, settings); - // store common expression for speed - $this = $(this); - // save the settings where they read - $.data(this, "tablesorter", config); - // build headers - $headers = buildHeaders(this); - // try to auto detect column type, and store in tables config - this.config.parsers = buildParserCache(this, $headers); - // build the cache for the tbody cells - cache = buildCache(this); - // get the css class names, could be done else where. - var sortCSS = [config.cssDesc, config.cssAsc]; - // fixate columns if the users supplies the fixedWidth option - fixColumnWidth(this); - // apply event handling to headers - // this is to big, perhaps break it out? - $headers.click( - - function (e) { - var totalRows = ($this[0].tBodies[0] && $this[0].tBodies[0].rows.length) || 0; - if (!this.sortDisabled && totalRows > 0) { - // Only call sortStart if sorting is - // enabled. - $this.trigger("sortStart"); - // store exp, for speed - var $cell = $(this); - // get current column index - var i = this.column; - // get current column sort order - this.order = this.count++ % 2; - // always sort on the locked order. - if(this.lockedOrder) this.order = this.lockedOrder; - - // user only whants to sort on one - // column - if (!e[config.sortMultiSortKey]) { - // flush the sort list - config.sortList = []; - if (config.sortForce != null) { - var a = config.sortForce; - for (var j = 0; j < a.length; j++) { - if (a[j][0] != i) { - config.sortList.push(a[j]); - } - } - } - // add column to sort list - config.sortList.push([i, this.order]); - // multi column sorting - } else { - // the user has clicked on an all - // ready sortet column. - if (isValueInArray(i, config.sortList)) { - // revers the sorting direction - // for all tables. - for (var j = 0; j < config.sortList.length; j++) { - var s = config.sortList[j], - o = config.headerList[s[0]]; - if (s[0] == i) { - o.count = s[1]; - o.count++; - s[1] = o.count % 2; - } - } - } else { - // add column to sort list array - config.sortList.push([i, this.order]); - } - }; - setTimeout(function () { - // set css for headers - setHeadersCss($this[0], $headers, config.sortList, sortCSS); - appendToTable( - $this[0], multisort( - $this[0], config.sortList, cache) - ); - }, 1); - // stop normal event by returning false - return false; - } - // cancel selection - }).mousedown(function () { - if (config.cancelSelection) { - this.onselectstart = function () { - return false - }; - return false; - } - }); - // apply easy methods that trigger binded events - $this.bind("update", function () { - var me = this; - setTimeout(function () { - // rebuild parsers. - me.config.parsers = buildParserCache( - me, $headers); - // rebuild the cache map - cache = buildCache(me); - }, 1); - }).bind("updateCell", function (e, cell) { - var config = this.config; - // get position from the dom. - var pos = [(cell.parentNode.rowIndex - 1), cell.cellIndex]; - // update cache - cache.normalized[pos[0]][pos[1]] = config.parsers[pos[1]].format( - getElementText(config, cell), cell); - }).bind("sorton", function (e, list) { - $(this).trigger("sortStart"); - config.sortList = list; - // update and store the sortlist - var sortList = config.sortList; - // update header count index - updateHeaderSortCount(this, sortList); - // set css for headers - setHeadersCss(this, $headers, sortList, sortCSS); - // sort the table and append it to the dom - appendToTable(this, multisort(this, sortList, cache)); - }).bind("appendCache", function () { - appendToTable(this, cache); - }).bind("applyWidgetId", function (e, id) { - getWidgetById(id).format(this); - }).bind("applyWidgets", function () { - // apply widgets - applyWidget(this); - }); - if ($.metadata && ($(this).metadata() && $(this).metadata().sortlist)) { - config.sortList = $(this).metadata().sortlist; - } - // if user has supplied a sort list to constructor. - if (config.sortList.length > 0) { - $this.trigger("sorton", [config.sortList]); - } - // apply widgets - applyWidget(this); - }); - }; - this.addParser = function (parser) { - var l = parsers.length, - a = true; - for (var i = 0; i < l; i++) { - if (parsers[i].id.toLowerCase() == parser.id.toLowerCase()) { - a = false; - } - } - if (a) { - parsers.push(parser); - }; - }; - this.addWidget = function (widget) { - widgets.push(widget); - }; - this.formatFloat = function (s) { - var i = parseFloat(s); - return (isNaN(i)) ? 0 : i; - }; - this.formatInt = function (s) { - var i = parseInt(s); - return (isNaN(i)) ? 0 : i; - }; - this.isDigit = function (s, config) { - // replace all an wanted chars and match. - return /^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g, ''))); - }; - this.clearTableBody = function (table) { - if ($.browser.msie) { - function empty() { - while (this.firstChild) - this.removeChild(this.firstChild); - } - empty.apply(table.tBodies[0]); - } else { - table.tBodies[0].innerHTML = ""; - } - }; - } - }); - - // extend plugin scope - $.fn.extend({ - tablesorter: $.tablesorter.construct - }); - - // make shortcut - var ts = $.tablesorter; - - // add default parsers - ts.addParser({ - id: "text", - is: function (s) { - return true; - }, format: function (s) { - return $.trim(s.toLocaleLowerCase()); - }, type: "text" - }); - - ts.addParser({ - id: "digit", - is: function (s, table) { - var c = table.config; - return $.tablesorter.isDigit(s, c); - }, format: function (s) { - return $.tablesorter.formatFloat(s); - }, type: "numeric" - }); - - ts.addParser({ - id: "currency", - is: function (s) { - return /^[£$€?.]/.test(s); - }, format: function (s) { - return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g), "")); - }, type: "numeric" - }); - - ts.addParser({ - id: "ipAddress", - is: function (s) { - return /^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s); - }, format: function (s) { - var a = s.split("."), - r = "", - l = a.length; - for (var i = 0; i < l; i++) { - var item = a[i]; - if (item.length == 2) { - r += "0" + item; - } else { - r += item; - } - } - return $.tablesorter.formatFloat(r); - }, type: "numeric" - }); - - ts.addParser({ - id: "url", - is: function (s) { - return /^(https?|ftp|file):\/\/$/.test(s); - }, format: function (s) { - return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//), '')); - }, type: "text" - }); - - ts.addParser({ - id: "isoDate", - is: function (s) { - return /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s); - }, format: function (s) { - return $.tablesorter.formatFloat((s != "") ? new Date(s.replace( - new RegExp(/-/g), "/")).getTime() : "0"); - }, type: "numeric" - }); - - ts.addParser({ - id: "percent", - is: function (s) { - return /\%$/.test($.trim(s)); - }, format: function (s) { - return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g), "")); - }, type: "numeric" - }); - - ts.addParser({ - id: "usLongDate", - is: function (s) { - return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/)); - }, format: function (s) { - return $.tablesorter.formatFloat(new Date(s).getTime()); - }, type: "numeric" - }); - - ts.addParser({ - id: "shortDate", - is: function (s) { - return /^\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}$/.test(s); - }, format: function (s, table) { - var c = table.config; - s = s.replace(/\-/g, "/"); - if (c.dateFormat == "us") { - // reformat the string in ISO format - s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$1/$2"); - } else if (c.dateFormat == "uk") { - // reformat the string in ISO format - s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1"); - } else if (c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") { - s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/, "$1/$2/$3"); - } - return $.tablesorter.formatFloat(new Date(s).getTime()); - }, type: "numeric" - }); - ts.addParser({ - id: "time", - is: function (s) { - return /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s); - }, format: function (s) { - return $.tablesorter.formatFloat(new Date("2000/01/01 " + s).getTime()); - }, type: "numeric" - }); - ts.addParser({ - id: "metadata", - is: function (s) { - return false; - }, format: function (s, table, cell) { - var c = table.config, - p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; - return $(cell).metadata()[p]; - }, type: "numeric" - }); - // add default widgets - ts.addWidget({ - id: "zebra", - format: function (table) { - if (table.config.debug) { - var time = new Date(); - } - var $tr, row = -1, - odd; - // loop through the visible rows - $("tr:visible", table.tBodies[0]).each(function (i) { - $tr = $(this); - // style children rows the same way the parent - // row was styled - if (!$tr.hasClass(table.config.cssChildRow)) row++; - odd = (row % 2 == 0); - $tr.removeClass( - table.config.widgetZebra.css[odd ? 0 : 1]).addClass( - table.config.widgetZebra.css[odd ? 1 : 0]) - }); - if (table.config.debug) { - $.tablesorter.benchmark("Applying Zebra widget", time); - } - } - }); -})(jQuery); diff --git a/sitestatic/jquery.tablesorter.min.js b/sitestatic/jquery.tablesorter.min.js deleted file mode 100644 index ffe991f7..00000000 --- a/sitestatic/jquery.tablesorter.min.js +++ /dev/null @@ -1,4 +0,0 @@ - -(function($){$.extend({tablesorter:new -function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",cssChildRow:"expand-child",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,sortLocaleCompare:true,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'/\.|\,/g',onRenderHeader:null,selectorHeaders:'thead th',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}if(table.tBodies.length==0)return;var rows=table.tBodies[0].rows;if(rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function checkHeaderOptionsSortingLocked(table,i){if((table.config.headers[i])&&(table.config.headers[i].lockedOrder))return table.config.headers[i].lockedOrder;return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;i b["+i+"]) ? 1 : 0));";};function makeSortTextDesc(i){return"((b["+i+"] < a["+i+"]) ? -1 : ((b["+i+"] > a["+i+"]) ? 1 : 0));";};function makeSortNumeric(i){return"a["+i+"]-b["+i+"];";};function makeSortNumericDesc(i){return"b["+i+"]-a["+i+"];";};function sortText(a,b){if(table.config.sortLocaleCompare)return a.localeCompare(b);return((ab)?1:0));};function sortTextDesc(a,b){if(table.config.sortLocaleCompare)return b.localeCompare(a);return((ba)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$.data(this,"tablesorter",config);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){$this.trigger("sortStart");var $cell=$(this);var i=this.column;this.order=this.count++%2;if(this.lockedOrder)this.order=this.lockedOrder;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;iDeveloper World Clocks $(document).ready(function() { $("#clocks-table:has(tbody tr)").tablesorter({ widgets: ['zebra'], + sortLocaleCompare: true, sortList: [[0,0]], headers: { 4: { sorter: false } } }); diff --git a/templates/devel/index.html b/templates/devel/index.html index ad8ee0ee..488b6755 100644 --- a/templates/devel/index.html +++ b/templates/devel/index.html @@ -306,11 +306,14 @@

    Stats by Developer

    sortList: [[0,0]], headers: { 6: {sorter: false } } }); - $(".dash-stats").tablesorter({ + var settings = { widgets: ['zebra'], sortList: [[0,0]], headers: { 1: { sorter: 'pkgcount' }, 2: { sorter: 'pkgcount' }, 3: { sorter: 'pkgcount' } } - }); + }; + $(".dash-stats").not($("#stats-by-maintainer")).tablesorter(settings); + settings['sortLocaleCompare'] = true; + $("#stats-by-maintainer").tablesorter(settings); }); {% endblock %} diff --git a/templates/public/keys.html b/templates/public/keys.html index cd0b0592..1fed3c15 100644 --- a/templates/public/keys.html +++ b/templates/public/keys.html @@ -94,6 +94,7 @@

    Master Signing Keys

    {% endblock %} -- cgit v1.2.3-54-g00ecf From 0b7939ae1a1e3ce55ee458d24fd5946542d9c14a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 19:01:45 -0500 Subject: Rework package details dispatch code We had a variety of fallback paths that we took if a details page didn't exist for the combination of URL parts passed in. Before I go adding a few more possibilities, rework this so it is more flexible. It is now as simple as adding a method to the dispatch options list in order to have further fallback options. Signed-off-by: Dan McGee --- packages/views/display.py | 82 ++++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/packages/views/display.py b/packages/views/display.py index 585e0e4e..1ea9ea48 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -14,10 +14,10 @@ def split_package_details(request, name, repo, arch): - arch = get_object_or_404(Arch, name=arch) + '''Check if we have a split package (e.g. pkgbase) value matching this + name. If so, we can show a listing page for the entire set of packages.''' arches = [ arch ] arches.extend(Arch.objects.filter(agnostic=True)) - repo = get_object_or_404(Repo, name__iexact=repo) pkgs = Package.objects.normal().filter(pkgbase=name, repo__testing=repo.testing, repo__staging=repo.staging, arch__in=arches).order_by('pkgname') @@ -39,15 +39,13 @@ def split_package_details(request, name, repo, arch): def recently_removed_package(request, name, repo, arch, cutoff=CUTOFF): - '''We're just steps away from raising a 404, but check our packages update - table first to see if this package has existed in this repo before. If so, - we can show a 410 Gone page and point the requester in the right - direction.''' - arch = get_object_or_404(Arch, name=arch) + '''Check our packages update table to see if this package has existed in + this repo before. If so, we can show a 410 Gone page and point the + requester in the right direction.''' arches = [ arch ] arches.extend(Arch.objects.filter(agnostic=True)) match = Update.objects.select_related('arch', 'repo').filter( - pkgname=name, repo__name__iexact=repo, arch__in=arches) + pkgname=name, repo=repo, arch__in=arches) if cutoff is not None: when = now() - cutoff match = match.filter(created__gte=when) @@ -59,43 +57,53 @@ def recently_removed_package(request, name, repo, arch, cutoff=CUTOFF): return None +def redirect_agnostic(request, name, repo, arch): + '''For arch='any' packages, we can issue a redirect to them if we have a + single non-ambiguous option by changing the arch to match any arch-agnostic + package.''' + if not arch.agnostic: + # limit to 2 results, we only need to know whether there is anything + # except only one matching result + pkgs = Package.objects.select_related( + 'arch', 'repo', 'packager').filter(pkgname=name, + repo=repo, arch__agnostic=True)[:2] + if len(pkgs) == 1: + return redirect(pkgs[0], permanent=True) + return None + + +def redirect_to_search(request, name, repo, arch): + pkg_data = [ + ('arch', arch.lower()), + ('repo', repo.lower()), + ('q', name), + ] + # only include non-blank values in the query we generate + pkg_data = [(x, y.encode('utf-8')) for x, y in pkg_data if y] + return redirect("/packages/?%s" % urlencode(pkg_data)) + + def details(request, name='', repo='', arch=''): if all([name, repo, arch]): + arch_obj = get_object_or_404(Arch, name=arch) + repo_obj = get_object_or_404(Repo, name__iexact=repo) try: pkg = Package.objects.select_related( 'arch', 'repo', 'packager').get(pkgname=name, - repo__name__iexact=repo, arch__name=arch) + repo=repo_obj, arch=arch_obj) return render(request, 'packages/details.html', {'pkg': pkg}) except Package.DoesNotExist: - arch_obj = get_object_or_404(Arch, name=arch) - # for arch='any' packages, we can issue a redirect to them if we - # have a single non-ambiguous option by changing the arch to match - # any arch-agnostic package - if not arch_obj.agnostic: - pkgs = Package.objects.select_related( - 'arch', 'repo', 'packager').filter(pkgname=name, - repo__name__iexact=repo, arch__agnostic=True) - if len(pkgs) == 1: - return redirect(pkgs[0], permanent=True) - # do we have a split package matching this criteria? - ret = split_package_details(request, name, repo, arch) - if ret is None: - # maybe we have a recently-removed package? - ret = recently_removed_package(request, name, repo, arch) - if ret is not None: - return ret - else: - # we've tried everything at this point, nothing to see - raise Http404 + # attempt a variety of fallback options before 404ing + options = (redirect_agnostic, split_package_details, + recently_removed_package) + for method in options: + ret = method(request, name, repo_obj, arch_obj) + if ret: + return ret + # we've tried everything at this point, nothing to see + raise Http404 else: - pkg_data = [ - ('arch', arch.lower()), - ('repo', repo.lower()), - ('q', name), - ] - # only include non-blank values in the query we generate - pkg_data = [(x, y.encode('utf-8')) for x, y in pkg_data if y] - return redirect("/packages/?%s" % urlencode(pkg_data)) + return redirect_to_search(request, name, repo, arch) def groups(request, arch=None): -- cgit v1.2.3-54-g00ecf From 5f410c000eaca4b5b25664f4dfb59cbe85ea034e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 19:12:27 -0500 Subject: Add package details redirect for package replacements This makes sense if there is only one available replacement. We could get more sophisticated and show the removed page if there are multiple replacements available. Additionally, automatically redirect if there was only one matching package for a given package update deletion object. Signed-off-by: Dan McGee --- packages/views/display.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/views/display.py b/packages/views/display.py index 1ea9ea48..d6922314 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -51,12 +51,24 @@ def recently_removed_package(request, name, repo, arch, cutoff=CUTOFF): match = match.filter(created__gte=when) try: match = match.latest() + elsewhere = match.elsewhere() + if len(elsewhere) == 1: + return redirect(elsewhere[0]) return render(request, 'packages/removed.html', {'update': match, }, status=410) except Update.DoesNotExist: return None +def replaced_package(request, name, repo, arch): + '''Check our package replacements to see if this is a package we used to + have but no longer do.''' + match = Package.objects.filter(replaces__name=name, repo=repo, arch=arch) + if len(match) == 1: + return redirect(match[0], permanent=True) + return None + + def redirect_agnostic(request, name, repo, arch): '''For arch='any' packages, we can issue a redirect to them if we have a single non-ambiguous option by changing the arch to match any arch-agnostic @@ -95,7 +107,7 @@ def details(request, name='', repo='', arch=''): except Package.DoesNotExist: # attempt a variety of fallback options before 404ing options = (redirect_agnostic, split_package_details, - recently_removed_package) + recently_removed_package, replaced_package) for method in options: ret = method(request, name, repo_obj, arch_obj) if ret: -- cgit v1.2.3-54-g00ecf From 5f85a1240da14e57760c2ba6585ae943d7a1d8c2 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 19:31:44 -0500 Subject: Reuse removed template for packages with multiple replacements For example, bitcoin-git in the Arch repos is currently marked replaced by both bitcoin-qt and bitcoin-daemon. This allows us to show a page with both options listed instead of a blank 404 page. Signed-off-by: Dan McGee --- packages/views/display.py | 24 ++++++++++++++++++++---- templates/packages/removed.html | 12 ++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/views/display.py b/packages/views/display.py index d6922314..8adf3bee 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -50,12 +50,19 @@ def recently_removed_package(request, name, repo, arch, cutoff=CUTOFF): when = now() - cutoff match = match.filter(created__gte=when) try: - match = match.latest() - elsewhere = match.elsewhere() + update = match.latest() + elsewhere = update.elsewhere() if len(elsewhere) == 1: return redirect(elsewhere[0]) - return render(request, 'packages/removed.html', - {'update': match, }, status=410) + context = { + 'update': update, + 'elsewhere': elsewhere, + 'name': name, + 'version': update.old_version, + 'arch': arch, + 'repo': repo, + } + return render(request, 'packages/removed.html', context, status=410) except Update.DoesNotExist: return None @@ -66,6 +73,15 @@ def replaced_package(request, name, repo, arch): match = Package.objects.filter(replaces__name=name, repo=repo, arch=arch) if len(match) == 1: return redirect(match[0], permanent=True) + elif len(match) > 1: + context = { + 'elsewhere': match, + 'name': name, + 'version': '', + 'arch': arch, + 'repo': repo, + } + return render(request, 'packages/removed.html', context, status=410) return None diff --git a/templates/packages/removed.html b/templates/packages/removed.html index 17b1f989..ea20ce80 100644 --- a/templates/packages/removed.html +++ b/templates/packages/removed.html @@ -2,17 +2,17 @@ {% load url from future %} {% load package_extras %} -{% block title %}Arch Linux - Not Available - {{ update.pkgname }} {{ update.old_version }} ({{ update.arch.name }}){% endblock %} +{% block title %}Arch Linux - Not Available - {{ name }} {{ version }} ({{ arch.name }}){% endblock %} {% block navbarclass %}anb-packages{% endblock %} {% block content %}
    -

    {{ update.pkgname }} {{ update.old_version }} is no longer available

    +

    {{ name }} {{ version }} is no longer available

    -

    {{ update.pkgname }} {{ update.old_version }} has been removed from the [{{ update.repo.name|lower }}] repository.

    +

    {{ name }} {{ version }} has been removed from the [{{ repo.name|lower }}] repository.

    - {% with update.elsewhere as elsewhere %}{% if elsewhere %} -

    However, this package is available in other repositories:

    + {% if elsewhere %} +

    However, this package or replacements are available elsewhere:

      {% for pkg in elsewhere %}
    • {% pkg_details_link pkg %} {{ pkg.full_version }} [{{ pkg.repo.name|lower }}] ({{ pkg.arch.name }})
    • @@ -23,6 +23,6 @@

      {{ update.pkgname }} {{ update.old_version }} is no longer available

      Try using the package search page, or try searching the AUR to see if the package can be found there.

      - {% endif %}{% endwith %} + {% endif %}
    {% endblock %} -- cgit v1.2.3-54-g00ecf From f5d3c02eb14ea8b0018e17fa9be9c511ad7ebff9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 19:49:37 -0500 Subject: Revert "Fall back to 410 Gone for package files view as well" This reverts commit 9ab460c53a1ac4c79da6f05f2850ee21beedbab2. This seemed like the right thing to do, but it doesn't really play well with our more general dispatch framework we now do on the package details pages. Just let it 404 like it always did, as these pages are less essential. We can perhaps add a full dispatcher later if we really feel the need. --- packages/views/display.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/views/display.py b/packages/views/display.py index 8adf3bee..b5cd643a 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -165,16 +165,8 @@ def group_details(request, arch, name): def files(request, name, repo, arch): - try: - pkg = Package.objects.get(pkgname=name, - repo__name__iexact=repo, arch__name=arch) - except Package.DoesNotExist: - # this may have been deleted recently, so follow the same logic as we - # do on the package details page if possible - ret = recently_removed_package(request, name, repo, arch) - if ret is not None: - return ret - raise Http404 + pkg = get_object_or_404(Package, + pkgname=name, repo__name__iexact=repo, arch__name=arch) # files are inserted in sorted order, so preserve that fileslist = PackageFile.objects.filter(pkg=pkg).order_by('id') dir_count = sum(1 for f in fileslist if f.is_directory) -- cgit v1.2.3-54-g00ecf From 686942b8788fa43031b3999ac00d60baadc82f53 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 19:56:49 -0500 Subject: Declare 'enums' at class scope Signed-off-by: Dan McGee --- mirrors/models.py | 15 +++++++-------- mirrors/views.py | 4 ++-- packages/models.py | 13 ++++++------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/mirrors/models.py b/mirrors/models.py index 9a545b51..06b483d5 100644 --- a/mirrors/models.py +++ b/mirrors/models.py @@ -6,15 +6,14 @@ from django_countries import CountryField -TIER_CHOICES = ( - (0, 'Tier 0'), - (1, 'Tier 1'), - (2, 'Tier 2'), - (-1, 'Untiered'), -) - - class Mirror(models.Model): + TIER_CHOICES = ( + (0, 'Tier 0'), + (1, 'Tier 1'), + (2, 'Tier 2'), + (-1, 'Untiered'), + ) + name = models.CharField(max_length=255, unique=True) tier = models.SmallIntegerField(default=2, choices=TIER_CHOICES) upstream = models.ForeignKey('self', null=True, on_delete=models.SET_NULL) diff --git a/mirrors/views.py b/mirrors/views.py index 400c084d..2c2577f4 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -12,7 +12,7 @@ from django.views.decorators.csrf import csrf_exempt from django_countries.countries import COUNTRIES -from .models import Mirror, MirrorUrl, MirrorProtocol, TIER_CHOICES +from .models import Mirror, MirrorUrl, MirrorProtocol from .utils import get_mirror_statuses, get_mirror_errors COUNTRY_LOOKUP = dict(COUNTRIES) @@ -186,7 +186,7 @@ def mirror_details(request, name): def status(request, tier=None): if tier is not None: tier = int(tier) - if tier not in [t[0] for t in TIER_CHOICES]: + if tier not in [t[0] for t in Mirror.TIER_CHOICES]: raise Http404 bad_timedelta = timedelta(days=3) status_info = get_mirror_statuses() diff --git a/packages/models.py b/packages/models.py index 5b48b30f..1d538cce 100644 --- a/packages/models.py +++ b/packages/models.py @@ -196,13 +196,6 @@ def __unicode__(self): return u'%s from %s on %s' % (self.pkgbase, self.who(), self.created) -UPDATE_ACTION_CHOICES = ( - (ADDITION, 'Addition'), - (CHANGE, 'Change'), - (DELETION, 'Deletion'), -) - - class UpdateManager(models.Manager): def log_update(self, old_pkg, new_pkg): '''Utility method to help log an update. This will determine the type @@ -249,6 +242,12 @@ def log_update(self, old_pkg, new_pkg): class Update(models.Model): + UPDATE_ACTION_CHOICES = ( + (ADDITION, 'Addition'), + (CHANGE, 'Change'), + (DELETION, 'Deletion'), + ) + package = models.ForeignKey(Package, related_name="updates", null=True, on_delete=models.SET_NULL) repo = models.ForeignKey(Repo, related_name="updates") -- cgit v1.2.3-54-g00ecf From 71859672267ccfc15e31398c5d86f5c0f69f0ed7 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 20:01:03 -0500 Subject: Use a raw ID field for package Update package Signed-off-by: Dan McGee --- packages/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/admin.py b/packages/admin.py index d43cecce..51c6fb06 100644 --- a/packages/admin.py +++ b/packages/admin.py @@ -44,6 +44,7 @@ class UpdateAdmin(admin.ModelAdmin): search_fields = ('pkgname',) ordering = ('-created',) date_hierarchy = 'created' + raw_id_fields = ('package',) admin.site.register(PackageRelation, PackageRelationAdmin) -- cgit v1.2.3-54-g00ecf From 566a9803dd4928fa2145ef14da2d59d2631eeb05 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 20:07:15 -0500 Subject: Add new deptype column to package depends This is more flexible than our existing 'optional' boolean and will allow us to import check and make depends into the database as well as what we are already doing. Signed-off-by: Dan McGee --- .../0020_auto__add_field_depend_deptype.py | 212 +++++++++++++++++++++ packages/models.py | 9 + 2 files changed, 221 insertions(+) create mode 100644 packages/migrations/0020_auto__add_field_depend_deptype.py diff --git a/packages/migrations/0020_auto__add_field_depend_deptype.py b/packages/migrations/0020_auto__add_field_depend_deptype.py new file mode 100644 index 00000000..4cc5bc17 --- /dev/null +++ b/packages/migrations/0020_auto__add_field_depend_deptype.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +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_depend', 'deptype', + self.gf('django.db.models.fields.CharField')(default='D', max_length=1), + keep_default=True) + + def backwards(self, orm): + db.delete_column('packages_depend', 'deptype') + + + 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',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + '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': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'", 'on_delete': 'models.PROTECT', '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': '2'}), + '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.depend': { + 'Meta': {'ordering': "['name']", 'object_name': 'Depend'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'deptype': ('django.db.models.fields.CharField', [], {'default': "'D'", 'max_length': '1'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'depends'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.flagrequest': { + 'Meta': {'object_name': 'FlagRequest'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'is_legitimate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'num_packages': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + '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', 'db_index': 'True'}), + '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'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'packages.update': { + 'Meta': {'object_name': 'Update'}, + 'action_flag': ('django.db.models.fields.PositiveSmallIntegerField', [], {}), + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Arch']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'new_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'new_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'old_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Package']"}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Repo']"}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index 1d538cce..b3752b6c 100644 --- a/packages/models.py +++ b/packages/models.py @@ -403,10 +403,19 @@ class Meta: class Depend(RelatedToBase): + DEPTYPE_CHOICES = ( + ('D', 'Depend'), + ('O', 'Optional Depend'), + ('M', 'Make Depend'), + ('C', 'Check Depend'), + ) + pkg = models.ForeignKey(Package, related_name='depends') comparison = models.CharField(max_length=255, default='') optional = models.BooleanField(default=False) description = models.TextField(null=True, blank=True) + deptype = models.CharField(max_length=1, default='D', + choices=DEPTYPE_CHOICES) class Conflict(RelatedToBase): -- cgit v1.2.3-54-g00ecf From a64bbbd4139d91cbbca10d804067cbd87a95872d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 20:27:43 -0500 Subject: Make adjustments for optional -> deptype conversion Very little dealt directly with this field. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 6 +- main/models.py | 9 +- packages/migrations/0021_migrate_optional_deps.py | 210 ++++++++++++++++++++++ templates/packages/details_depend.html | 2 +- templates/packages/details_requiredby.html | 2 +- 5 files changed, 222 insertions(+), 7 deletions(-) create mode 100644 packages/migrations/0021_migrate_optional_deps.py diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index aaa9812e..a3bf3e0c 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -145,8 +145,8 @@ def full_version(self): DEPEND_RE = re.compile(r"^(.+?)((>=|<=|=|>|<)(.+))?$") -def create_depend(package, dep_str, optional=False): - depend = Depend(pkg=package, optional=optional) +def create_depend(package, dep_str, deptype='D'): + depend = Depend(pkg=package, deptype=deptype) # lop off any description first parts = dep_str.split(':', 1) if len(parts) > 1: @@ -257,7 +257,7 @@ def populate_pkg(dbpkg, repopkg, force=False, timestamp=None): dbpkg.depends.all().delete() deps = [create_depend(dbpkg, y) for y in repopkg.depends] - deps += [create_depend(dbpkg, y, True) for y in repopkg.optdepends] + deps += [create_depend(dbpkg, y, 'O') for y in repopkg.optdepends] batched_bulk_create(Depend, deps) dbpkg.conflicts.all().delete() diff --git a/main/models.py b/main/models.py index 577f11c6..f4ced350 100644 --- a/main/models.py +++ b/main/models.py @@ -245,13 +245,18 @@ def get_depends(self): deps = [] arches = None # TODO: we can use list comprehension and an 'in' query to make this more effective - for dep in self.depends.order_by('optional', 'name'): + for dep in self.depends.all(): pkg = dep.get_best_satisfier() providers = None if not pkg: providers = dep.get_providers() deps.append({'dep': dep, 'pkg': pkg, 'providers': providers}) - return deps + # sort the list; deptype sorting makes this tricker than expected + sort_order = {'D': 0, 'O': 1, 'M': 2, 'C': 3} + def sort_key(val): + dep = val['dep'] + return (sort_order.get(dep.deptype, 1000), dep.name) + return sorted(deps, key=sort_key) @cache_function(125) def base_package(self): diff --git a/packages/migrations/0021_migrate_optional_deps.py b/packages/migrations/0021_migrate_optional_deps.py new file mode 100644 index 00000000..f6652ce1 --- /dev/null +++ b/packages/migrations/0021_migrate_optional_deps.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + orm['packages.Depend'].objects.filter(optional=False).update(deptype='D') + orm['packages.Depend'].objects.filter(optional=True).update(deptype='O') + + def backwards(self, orm): + orm['packages.Depend'].objects.all().update(deptype='D') + + 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',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + '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': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'", 'on_delete': 'models.PROTECT', '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': '2'}), + '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.depend': { + 'Meta': {'ordering': "['name']", 'object_name': 'Depend'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'deptype': ('django.db.models.fields.CharField', [], {'default': "'D'", 'max_length': '1'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'depends'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.flagrequest': { + 'Meta': {'object_name': 'FlagRequest'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'is_legitimate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'num_packages': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + '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', 'db_index': 'True'}), + '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'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'packages.update': { + 'Meta': {'object_name': 'Update'}, + 'action_flag': ('django.db.models.fields.PositiveSmallIntegerField', [], {}), + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Arch']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'new_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'new_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'old_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Package']"}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Repo']"}) + } + } + + complete_apps = ['packages'] + symmetrical = True diff --git a/templates/packages/details_depend.html b/templates/packages/details_depend.html index 0cf2c36a..1eb35474 100644 --- a/templates/packages/details_depend.html +++ b/templates/packages/details_depend.html @@ -11,6 +11,6 @@ {% if depend.pkg.repo.testing %} (testing){% endif %} {% if depend.pkg.repo.staging %} (staging){% endif %} {% endifequal %} -{% if depend.dep.optional %} (optional){% endif %} +{% if depend.dep.deptype == 'O' %} (optional){% endif %} {% if depend.dep.description %}- {{ depend.dep.description }}{% endif %} diff --git a/templates/packages/details_requiredby.html b/templates/packages/details_requiredby.html index ecc92b29..15c62c61 100644 --- a/templates/packages/details_requiredby.html +++ b/templates/packages/details_requiredby.html @@ -3,5 +3,5 @@ {% if req.name != pkg.pkgname %}(requires {{ req.name }}){% endif %} {% if req.pkg.repo.testing %}(testing){% endif %} {% if req.pkg.repo.staging %}(staging){% endif %} -{% if req.optional %}(optional){% endif %} +{% if req.deptype == 'O' %}(optional){% endif %} -- cgit v1.2.3-54-g00ecf From 4c02b11cd6c53e122e1c919b64e28646960c5eda Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 20:29:53 -0500 Subject: Remove optional package depends column This is now completely replaced by the deptype column. Signed-off-by: Dan McGee --- .../0022_auto__del_field_depend_optional.py | 211 +++++++++++++++++++++ packages/models.py | 1 - 2 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 packages/migrations/0022_auto__del_field_depend_optional.py diff --git a/packages/migrations/0022_auto__del_field_depend_optional.py b/packages/migrations/0022_auto__del_field_depend_optional.py new file mode 100644 index 00000000..8e65ccb1 --- /dev/null +++ b/packages/migrations/0022_auto__del_field_depend_optional.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.delete_column('packages_depend', 'optional') + + def backwards(self, orm): + db.add_column('packages_depend', 'optional', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + 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',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + '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': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'", 'on_delete': 'models.PROTECT', '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': '2'}), + '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.depend': { + 'Meta': {'ordering': "['name']", 'object_name': 'Depend'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'deptype': ('django.db.models.fields.CharField', [], {'default': "'D'", 'max_length': '1'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + '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': "'depends'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.flagrequest': { + 'Meta': {'object_name': 'FlagRequest'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'is_legitimate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'num_packages': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + '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', 'db_index': 'True'}), + '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'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'packages.update': { + 'Meta': {'object_name': 'Update'}, + 'action_flag': ('django.db.models.fields.PositiveSmallIntegerField', [], {}), + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Arch']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'new_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'new_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'old_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Package']"}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Repo']"}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index b3752b6c..65aa8f4a 100644 --- a/packages/models.py +++ b/packages/models.py @@ -412,7 +412,6 @@ class Depend(RelatedToBase): pkg = models.ForeignKey(Package, related_name='depends') comparison = models.CharField(max_length=255, default='') - optional = models.BooleanField(default=False) description = models.TextField(null=True, blank=True) deptype = models.CharField(max_length=1, default='D', choices=DEPTYPE_CHOICES) -- cgit v1.2.3-54-g00ecf From 1f2466fffceafebfaca34e3ed2d34de6b622768b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 31 Jul 2012 20:35:50 -0500 Subject: reporead: import make and check depends We don't have these in the database yet, but future verisons of repo-add will put this information in the sync databases. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index a3bf3e0c..8b55b09a 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -80,8 +80,9 @@ class RepoPackage(object): bare = ( 'name', 'base', 'arch', 'filename', 'md5sum', 'sha256sum', 'url', 'packager' ) number = ( 'csize', 'isize' ) - collections = ( 'depends', 'optdepends', 'conflicts', - 'provides', 'replaces', 'groups', 'license', 'files' ) + collections = ( 'depends', 'optdepends', 'makedepends', 'checkdepends', + 'conflicts', 'provides', 'replaces', 'groups', 'license', + 'files' ) version_re = re.compile(r'^((\d+):)?(.+)-([^-]+)$') @@ -258,6 +259,8 @@ def populate_pkg(dbpkg, repopkg, force=False, timestamp=None): dbpkg.depends.all().delete() deps = [create_depend(dbpkg, y) for y in repopkg.depends] deps += [create_depend(dbpkg, y, 'O') for y in repopkg.optdepends] + deps += [create_depend(dbpkg, y, 'M') for y in repopkg.makedepends] + deps += [create_depend(dbpkg, y, 'C') for y in repopkg.checkdepends] batched_bulk_create(Depend, deps) dbpkg.conflicts.all().delete() -- cgit v1.2.3-54-g00ecf From c130414a478080519219872ebd842d139d1f139e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 4 Aug 2012 14:43:21 -0500 Subject: Make tablesorter filesize sorter more flexible Accept a few more prefixes, and also handle both 'MB' and 'MiB' style sizes. Signed-off-by: Dan McGee --- sitestatic/archweb.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/sitestatic/archweb.js b/sitestatic/archweb.js index 9e9bddf6..1ddf4ebf 100644 --- a/sitestatic/archweb.js +++ b/sitestatic/archweb.js @@ -92,7 +92,7 @@ if (typeof $.tablesorter !== 'undefined') { }); $.tablesorter.addParser({ id: 'filesize', - re: /^(\d+(?:\.\d+)?) (bytes?|KB|MB|GB|TB|PB)$/, + re: /^(\d+(?:\.\d+)?) (bytes?|[KMGTPEZY]i?B)$/, is: function(s) { return this.re.test(s); }, @@ -106,15 +106,29 @@ if (typeof $.tablesorter !== 'undefined') { switch(suffix) { /* intentional fall-through at each level */ + case 'YB': + case 'YiB': + size *= 1024; + case 'ZB': + case 'ZiB': + size *= 1024; + case 'EB': + case 'EiB': + size *= 1024; case 'PB': + case 'PiB': size *= 1024; case 'TB': + case 'TiB': size *= 1024; case 'GB': + case 'GiB': size *= 1024; case 'MB': + case 'MiB': size *= 1024; case 'KB': + case 'KiB': size *= 1024; } return size; -- cgit v1.2.3-54-g00ecf From f61e61c8a6fc0753359963a836bf65a3a8b1981e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 4 Aug 2012 15:07:03 -0500 Subject: Include description in Depend unicode() output This overrides the base class __unicode__ method. Signed-off-by: Dan McGee --- packages/models.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/models.py b/packages/models.py index 65aa8f4a..1959183f 100644 --- a/packages/models.py +++ b/packages/models.py @@ -416,6 +416,13 @@ class Depend(RelatedToBase): deptype = models.CharField(max_length=1, default='D', choices=DEPTYPE_CHOICES) + def __unicode__(self): + '''For depends, we may also have a description and a modifier.''' + to_str = super(Depend, self).__unicode__() + if self.description: + return u'%s: %s' % (to_str, self.description) + return to_str + class Conflict(RelatedToBase): pkg = models.ForeignKey(Package, related_name='conflicts') -- cgit v1.2.3-54-g00ecf From 4613f862d76b6ab5de7dc98021fa37341945a2c2 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 4 Aug 2012 15:14:53 -0500 Subject: Add support to templates for make/check depends Signed-off-by: Dan McGee --- sitestatic/archweb.css | 2 ++ templates/packages/details_depend.html | 2 ++ templates/packages/details_requiredby.html | 2 ++ 3 files changed, 6 insertions(+) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 91cacd7b..70caf8fc 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -799,6 +799,8 @@ form#flag-pkg-form input[type=text] { #pkgdetails #metadata .testing-dep, #pkgdetails #metadata .staging-dep, #pkgdetails #metadata .opt-dep, +#pkgdetails #metadata .make-dep, +#pkgdetails #metadata .check-dep, #pkgdetails #metadata .dep-desc { font-style: italic; } diff --git a/templates/packages/details_depend.html b/templates/packages/details_depend.html index 1eb35474..a26d67bd 100644 --- a/templates/packages/details_depend.html +++ b/templates/packages/details_depend.html @@ -12,5 +12,7 @@ {% if depend.pkg.repo.staging %} (staging){% endif %} {% endifequal %} {% if depend.dep.deptype == 'O' %} (optional){% endif %} +{% if depend.dep.deptype == 'M' %} (make){% endif %} +{% if depend.dep.deptype == 'C' %} (check){% endif %} {% if depend.dep.description %}- {{ depend.dep.description }}{% endif %} diff --git a/templates/packages/details_requiredby.html b/templates/packages/details_requiredby.html index 15c62c61..24f8bbb1 100644 --- a/templates/packages/details_requiredby.html +++ b/templates/packages/details_requiredby.html @@ -4,4 +4,6 @@ {% if req.pkg.repo.testing %}(testing){% endif %} {% if req.pkg.repo.staging %}(staging){% endif %} {% if req.deptype == 'O' %}(optional){% endif %} +{% if req.deptype == 'M' %}(make){% endif %} +{% if req.deptype == 'C' %}(check){% endif %} -- cgit v1.2.3-54-g00ecf From c51ffa07fc46eeab001b1e71ebc0cee1d408aaea Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 4 Aug 2012 15:28:53 -0500 Subject: Adjust depends & required by templates to spew less whitespace Use the spaceless tag and structure the {% if %} blocks smartly so spaceless actually works and we aren't outputting one blank line per if statement. Signed-off-by: Dan McGee --- templates/packages/details_depend.html | 28 ++++++++++++---------------- templates/packages/details_requiredby.html | 17 ++++++++--------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/templates/packages/details_depend.html b/templates/packages/details_depend.html index a26d67bd..420af93c 100644 --- a/templates/packages/details_depend.html +++ b/templates/packages/details_depend.html @@ -1,18 +1,14 @@ -{% load package_extras %} -
  • +{% load package_extras %}{% spaceless %}
  • {% ifequal depend.pkg None %} -{% if depend.providers %} -{{ depend.dep.name }}{{ depend.dep.comparison|default:"" }}{{ depend.dep.version|default:"" }} ({% multi_pkg_details depend.providers %}) -{% else %} -{{ depend.dep.name }}{{ depend.dep.comparison|default:"" }}{{ depend.dep.version|default:"" }} (virtual) -{% endif %} -{% else %} +{% if depend.providers %}{{ depend.dep.name }}{{ depend.dep.comparison|default:"" }}{{ depend.dep.version|default:"" }} ({% multi_pkg_details depend.providers %}) +{% else %}{{ depend.dep.name }}{{ depend.dep.comparison|default:"" }}{{ depend.dep.version|default:"" }} (virtual) +{% endif %}{% else %} {% pkg_details_link depend.pkg %}{{ depend.dep.comparison|default:"" }}{{ depend.dep.version|default:"" }} -{% if depend.pkg.repo.testing %} (testing){% endif %} -{% if depend.pkg.repo.staging %} (staging){% endif %} -{% endifequal %} -{% if depend.dep.deptype == 'O' %} (optional){% endif %} -{% if depend.dep.deptype == 'M' %} (make){% endif %} -{% if depend.dep.deptype == 'C' %} (check){% endif %} -{% if depend.dep.description %}- {{ depend.dep.description }}{% endif %} -
  • +{% if depend.pkg.repo.testing %} (testing) +{% endif %}{% if depend.pkg.repo.staging %} (staging) +{% endif %}{% endifequal %} +{% if depend.dep.deptype == 'O' %} (optional) +{% endif %}{% if depend.dep.deptype == 'M' %} (make) +{% endif %}{% if depend.dep.deptype == 'C' %} (check) +{% endif %}{% if depend.dep.description %}- {{ depend.dep.description }} +{% endif %}{% endspaceless %} diff --git a/templates/packages/details_requiredby.html b/templates/packages/details_requiredby.html index 24f8bbb1..f498bd5e 100644 --- a/templates/packages/details_requiredby.html +++ b/templates/packages/details_requiredby.html @@ -1,9 +1,8 @@ -{% load package_extras %} -
  • {% pkg_details_link req.pkg %} -{% if req.name != pkg.pkgname %}(requires {{ req.name }}){% endif %} -{% if req.pkg.repo.testing %}(testing){% endif %} -{% if req.pkg.repo.staging %}(staging){% endif %} -{% if req.deptype == 'O' %}(optional){% endif %} -{% if req.deptype == 'M' %}(make){% endif %} -{% if req.deptype == 'C' %}(check){% endif %} -
  • +{% load package_extras %}{% spaceless %}
  • {% pkg_details_link req.pkg %} +{% if req.name != pkg.pkgname %}(requires {{ req.name }}) +{% endif %}{% if req.pkg.repo.testing %}(testing) +{% endif %}{% if req.pkg.repo.staging %}(staging) +{% endif %}{% if req.deptype == 'O' %}(optional) +{% endif %}{% if req.deptype == 'M' %}(make) +{% endif %}{% if req.deptype == 'C' %}(check) +{% endif %}
  • {% endspaceless %} -- cgit v1.2.3-54-g00ecf From d557d267c906f2844331cafb57a81dc5a43bb125 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Sat, 4 Aug 2012 08:53:22 +0200 Subject: Update download page for 2012.08.04 iso image * the -netinstall suffix was removed from the filename * the wiki entry about the "Arch Install Scripts" was redirected to "Installation Guide" Signed-off-by: Pierre Schmitz Signed-off-by: Dan McGee --- templates/public/download.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/public/download.html b/templates/public/download.html index e6c4b18d..f5f6f4d1 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -7,7 +7,7 @@ {% block navbarclass %}anb-download{% endblock %} {% block content %} -{% with version="2012.07.15" kernel_version="3.4.4" %} +{% with version="2012.08.04" kernel_version="3.4.7" %}

    Arch Linux Downloads

    @@ -21,8 +21,8 @@

    Release Info

    • Current Release: {{ version }}
    • -
    • Included Kernel: {{ kernel_version }}
    • -
    • Arch Install Scripts
    • +
    • Included Kernel: {{ kernel_version }}
    • +
    • Installation Guide
    • Resources:
      • BitTorrent Download (recommended) download is finished, so you can seed it back to others. A web-seed capable client is recommended for fastest download speeds.

        Test ISO Info

        @@ -75,7 +75,7 @@

        Checksums

        File integrity checksums for the latest releases can be found below:

          -
        • PGP signature
        • SHA1 checksums
        • -- cgit v1.2.3-54-g00ecf From cdbbf81f49a9b82fcadcdb795de5d543cd0af916 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 4 Aug 2012 18:49:11 -0500 Subject: Restore proper whitespace in depends/required by display Signed-off-by: Dan McGee --- templates/packages/details_depend.html | 12 ++++++------ templates/packages/details_requiredby.html | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/templates/packages/details_depend.html b/templates/packages/details_depend.html index 420af93c..4aa739c2 100644 --- a/templates/packages/details_depend.html +++ b/templates/packages/details_depend.html @@ -4,11 +4,11 @@ {% else %}{{ depend.dep.name }}{{ depend.dep.comparison|default:"" }}{{ depend.dep.version|default:"" }} (virtual) {% endif %}{% else %} {% pkg_details_link depend.pkg %}{{ depend.dep.comparison|default:"" }}{{ depend.dep.version|default:"" }} -{% if depend.pkg.repo.testing %} (testing) -{% endif %}{% if depend.pkg.repo.staging %} (staging) +{% if depend.pkg.repo.testing %} (testing) +{% endif %}{% if depend.pkg.repo.staging %} (staging) {% endif %}{% endifequal %} -{% if depend.dep.deptype == 'O' %} (optional) -{% endif %}{% if depend.dep.deptype == 'M' %} (make) -{% endif %}{% if depend.dep.deptype == 'C' %} (check) -{% endif %}{% if depend.dep.description %}- {{ depend.dep.description }} +{% if depend.dep.deptype == 'O' %} (optional) +{% endif %}{% if depend.dep.deptype == 'M' %} (make) +{% endif %}{% if depend.dep.deptype == 'C' %} (check) +{% endif %}{% if depend.dep.description %} - {{ depend.dep.description }} {% endif %}{% endspaceless %} diff --git a/templates/packages/details_requiredby.html b/templates/packages/details_requiredby.html index f498bd5e..e8c713ac 100644 --- a/templates/packages/details_requiredby.html +++ b/templates/packages/details_requiredby.html @@ -1,8 +1,8 @@ {% load package_extras %}{% spaceless %}
        • {% pkg_details_link req.pkg %} -{% if req.name != pkg.pkgname %}(requires {{ req.name }}) -{% endif %}{% if req.pkg.repo.testing %}(testing) -{% endif %}{% if req.pkg.repo.staging %}(staging) -{% endif %}{% if req.deptype == 'O' %}(optional) -{% endif %}{% if req.deptype == 'M' %}(make) -{% endif %}{% if req.deptype == 'C' %}(check) +{% if req.name != pkg.pkgname %} (requires {{ req.name }}) +{% endif %}{% if req.pkg.repo.testing %} (testing) +{% endif %}{% if req.pkg.repo.staging %} (staging) +{% endif %}{% if req.deptype == 'O' %} (optional) +{% endif %}{% if req.deptype == 'M' %} (make) +{% endif %}{% if req.deptype == 'C' %} (check) {% endif %}
        • {% endspaceless %} -- cgit v1.2.3-54-g00ecf From a87864f6d05250c9b07b8b9eaecae669505c7f38 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 4 Aug 2012 19:44:55 -0500 Subject: Slight adjustments to sitemaps priority values Signed-off-by: Dan McGee --- sitemaps.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/sitemaps.py b/sitemaps.py index 424168bf..97ab43c0 100644 --- a/sitemaps.py +++ b/sitemaps.py @@ -60,8 +60,6 @@ def location(self, obj): class NewsSitemap(Sitemap): - priority = "0.8" - def __init__(self): now = datetime.utcnow().replace(tzinfo=utc) self.one_day_ago = now - timedelta(days=1) @@ -73,6 +71,11 @@ def items(self): def lastmod(self, obj): return obj.last_modified + def priority(self, obj): + if obj.last_modified > self.one_week_ago: + return "0.9" + return "0.8" + def changefreq(self, obj): if obj.last_modified > self.one_day_ago: return 'daily' @@ -82,9 +85,12 @@ def changefreq(self, obj): class BaseSitemap(Sitemap): + DEFAULT_PRIORITY = 0.7 + base_viewnames = ( ('index', 1.0, 'hourly'), ('packages-search', 0.8, 'hourly'), + ('page-download', 0.8, 'monthly'), ('page-keys', 0.8, 'weekly'), ('news-list', 0.7, 'weekly'), ('groups-list', 0.5, 'weekly'), @@ -96,7 +102,6 @@ class BaseSitemap(Sitemap): 'page-tus', 'page-fellows', 'page-donate', - 'page-download', 'feeds-list', 'mirror-list', 'mirrorlist', @@ -117,7 +122,7 @@ def location(self, obj): def priority(self, obj): if isinstance(obj, tuple): return obj[1] - return 0.7 + return self.DEFAULT_PRIORITY def changefreq(self, obj): if isinstance(obj, tuple): -- cgit v1.2.3-54-g00ecf From 8ca64af397718f7dda0080467d202c6a70a33c8c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 5 Aug 2012 11:21:59 -0500 Subject: Smarter handling of multilib packages in "Versions Elsewhere" We can do some manipulation of the pkgname to ensure multilib packages show up here, as well as showing the non-multilib versions in the list when viewing the multilib packages. Signed-off-by: Dan McGee --- main/models.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/main/models.py b/main/models.py index f4ced350..7d8ea755 100644 --- a/main/models.py +++ b/main/models.py @@ -336,8 +336,16 @@ def in_staging(self): def elsewhere(self): '''attempt to locate this package anywhere else, regardless of architecture or repository. Excludes this package from the list.''' + names = [self.pkgname] + if self.pkgname.startswith('lib32-'): + names.append(self.pkgname[6:]) + elif self.pkgname.endswith('-multilib'): + names.append(self.pkgname[:-9]) + else: + names.append('lib32-' + self.pkgname) + names.append(self.pkgname + '-multilib') return Package.objects.normal().filter( - pkgname=self.pkgname).exclude(id=self.id).order_by( + pkgname__in=names).exclude(id=self.id).order_by( 'arch__name', 'repo__name') class PackageFile(models.Model): -- cgit v1.2.3-54-g00ecf From 7d9ed0b881bd05878e7a54f785c2551bc6e336d6 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 7 Aug 2012 01:16:51 -0500 Subject: Add reverse conflicts to package details This is a place where calling vercmp could come in really handy. Signed-off-by: Dan McGee --- main/models.py | 10 ++++++++++ templates/packages/details.html | 13 ++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/main/models.py b/main/models.py index 7d8ea755..b49edde3 100644 --- a/main/models.py +++ b/main/models.py @@ -258,6 +258,16 @@ def sort_key(val): return (sort_order.get(dep.deptype, 1000), dep.name) return sorted(deps, key=sort_key) + @cache_function(123) + def reverse_conflicts(self): + """ + Returns a list of packages with conflicts against this package. + """ + # TODO: fix this; right now we cheat since we can't do proper version + # number checking without using alpm or vercmp directly. + return Package.objects.filter(conflicts__name=self.pkgname, + conflicts__comparison='').distinct() + @cache_function(125) def base_package(self): """ diff --git a/templates/packages/details.html b/templates/packages/details.html index 201e3074..9e898b7f 100644 --- a/templates/packages/details.html +++ b/templates/packages/details.html @@ -126,16 +126,23 @@

          Versions Elsewhere

    {% endif %}{% endwith %} + {% with pkg.replaces.all as all_related %}{% if all_related %} + + + + + {% endif %}{% endwith %} {% with pkg.conflicts.all as all_related %}{% if all_related %} {% endif %}{% endwith %} - {% with pkg.replaces.all as all_related %}{% if all_related %} + {% with pkg.reverse_conflicts as rev_conflicts %}{% if rev_conflicts %} - - + + {% endif %}{% endwith %} -- cgit v1.2.3-54-g00ecf From 2cb15547b0d3dfee46f3d51341bebd0b1251c0f9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 7 Aug 2012 01:53:26 -0500 Subject: Add ctypes-based wrapper to ALPM vercmp API This will allow us to do some additional stuff on systems that have libalpm available; namely we can use the version comparison logic it provides to do smarter filtering etc. of fields that use comparsion operations. Signed-off-by: Dan McGee --- packages/alpm.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 packages/alpm.py diff --git a/packages/alpm.py b/packages/alpm.py new file mode 100644 index 00000000..a7f4c3b5 --- /dev/null +++ b/packages/alpm.py @@ -0,0 +1,68 @@ +import ctypes +from ctypes.util import find_library +import operator + + +def load_alpm(name=None): + # Load the alpm library and set up some of the functions we might use + if name == None: + name = find_library('alpm') + try: + alpm = ctypes.cdll.LoadLibrary(name) + except OSError: + return None + alpm.alpm_version.argtypes = () + alpm.alpm_version.restype = ctypes.c_char_p + alpm.alpm_pkg_vercmp.argtypes = (ctypes.c_char_p, ctypes.c_char_p) + alpm.alpm_pkg_vercmp.restype = ctypes.c_int + return alpm + + +ALPM = load_alpm() + +class AlpmAPI(object): + OPERATOR_MAP = { + '=': operator.eq, + '==': operator.eq, + '!=': operator.ne, + '<': operator.lt, + '<=': operator.le, + '>': operator.gt, + '>=': operator.ge, + } + + def __init__(self): + self.alpm = ALPM + self.available = ALPM is not None + + def version(self): + if not self.available: + return None + return ALPM.alpm_version() + + def vercmp(self, ver1, ver2): + if not self.available: + return None + return ALPM.alpm_pkg_vercmp(str(ver1), str(ver2)) + + def compare_versions(self, ver1, oper, ver2): + func = self.OPERATOR_MAP.get(oper, None) + if func is None: + raise Exception("Invalid operator %s specified" % oper) + if not self.available: + return None + res = self.vercmp(ver1, ver2) + return func(res, 0) + + +def main(): + api = AlpmAPI() + print api.version() + print api.vercmp(1, 2) + print api.compare_versions(1, '<', 2) + + +if __name__ == '__main__': + main() + +# vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From f63a70f5118781fe34d82ae0d59c911f0ea28d1d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 7 Aug 2012 19:36:07 -0500 Subject: Make use of new ctypes ALPM API We can use this when filtering down lists of depends, required by, conflicts, etc. to ensure we are honoring the version specifications the same way pacman would. Signed-off-by: Dan McGee --- main/models.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++------- packages/models.py | 23 +++++++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/main/models.py b/main/models.py index b49edde3..bb2320e8 100644 --- a/main/models.py +++ b/main/models.py @@ -11,6 +11,7 @@ from .fields import PositiveBigIntegerField from .utils import cache_function, set_created_field +from packages.alpm import AlpmAPI class TodolistManager(models.Manager): @@ -189,16 +190,42 @@ def get_requiredby(self): category as this package if that check makes sense. """ from packages.models import Depend - provides = set(self.provides.values_list('name', flat=True)) - provides.add(self.pkgname) + provides = self.provides.all() + provide_names = set(provide.name for provide in provides) + provide_names.add(self.pkgname) requiredby = Depend.objects.select_related('pkg', 'pkg__arch', 'pkg__repo').filter( - name__in=provides).order_by( + name__in=provide_names).order_by( '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()) + + # if we can use ALPM, ensure our returned Depend objects abide by the + # version comparison operators they may specify + alpm = AlpmAPI() + if alpm.available: + new_rqd = [] + for dep in requiredby: + if not dep.comparison or not dep.version: + # no comparisson/version, so always let it through + new_rqd.append(dep) + elif self.pkgname == dep.name: + # depends on this package, so check it directly + if alpm.compare_versions(self.full_version, + dep.comparison, dep.version): + new_rqd.append(dep) + else: + # it must be a provision of ours at this point + for provide in (p for p in provides if p.name == dep.name): + print(provide.version, dep.comparison, dep.version) + if alpm.compare_versions(provide.version, + dep.comparison, dep.version): + new_rqd.append(dep) + break + requiredby = new_rqd + # sort out duplicate packages; this happens if something has a double # versioned depend such as a kernel module requiredby = [list(vals)[0] for _, vals in @@ -263,10 +290,24 @@ def reverse_conflicts(self): """ Returns a list of packages with conflicts against this package. """ - # TODO: fix this; right now we cheat since we can't do proper version - # number checking without using alpm or vercmp directly. - return Package.objects.filter(conflicts__name=self.pkgname, - conflicts__comparison='').distinct() + pkgs = Package.objects.filter(conflicts__name=self.pkgname) + alpm = AlpmAPI() + if not alpm.available: + return pkgs + + # If we can use ALPM, we can filter out items that don't actually + # conflict due to the version specification. + pkgs = pkgs.prefetch_related('conflicts') + new_pkgs = [] + for package in pkgs: + for conflict in package.conflicts.all(): + if conflict.name != self.pkgname: + continue + if not conflict.comparison or not conflict.version \ + or alpm.compare_versions(self.full_version, + conflict.comparison, conflict.version): + new_pkgs.append(package) + return new_pkgs @cache_function(125) def base_package(self): diff --git a/packages/models.py b/packages/models.py index 1959183f..403dfef0 100644 --- a/packages/models.py +++ b/packages/models.py @@ -7,6 +7,7 @@ from main.models import Arch, Repo, Package from main.utils import set_created_field, database_vendor +from packages.alpm import AlpmAPI class PackageRelation(models.Model): @@ -341,6 +342,13 @@ def get_best_satisfier(self): # make sure we match architectures if possible arches = self.pkg.applicable_arches() pkgs = pkgs.filter(arch__in=arches) + # if we have a comparison operation, make sure the packages we grab + # actually satisfy the requirements + if self.comparison and self.version: + alpm = AlpmAPI() + pkgs = [pkg for pkg in pkgs if not alpm.available or + alpm.compare_versions(pkg.full_version, self.comparison, + self.version)] if len(pkgs) == 0: # couldn't find a package in the DB # it should be a virtual depend (or a removed package) @@ -373,6 +381,21 @@ def get_providers(self): arches = self.pkg.applicable_arches() pkgs = pkgs.filter(arch__in=arches) + # If we have a comparison operation, make sure the packages we grab + # actually satisfy the requirements. + alpm = AlpmAPI() + if alpm.available and self.comparison and self.version: + pkgs = pkgs.prefetch_related('provides') + new_pkgs = [] + for package in pkgs: + for provide in package.provides.all(): + if provide.name != self.name: + continue + if alpm.compare_versions(provide.version, + self.comparison, self.version): + new_pkgs.append(package) + pkgs = new_pkgs + # Logic here is to filter out packages that are in multiple repos if # they are not requested. For example, if testing is False, only show a # testing package if it doesn't exist in a non-testing repo. -- cgit v1.2.3-54-g00ecf From 94ea63afdd1f07392f2212dd69c3807986c161d4 Mon Sep 17 00:00:00 2001 From: Thomas Bächler Date: Tue, 7 Aug 2012 22:08:24 +0200 Subject: Add a link to netboot outside of the "Test ISO" section. Signed-off-by: Dan McGee --- templates/public/download.html | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/templates/public/download.html b/templates/public/download.html index f5f6f4d1..79a82f8b 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -49,6 +49,14 @@

    BitTorrent Download (recommended)

    title="Download for both architectures">Download archlinux-{{version}}-dual.iso.torrent +

    Netboot

    + +

    If you have a wired connection, you can boot the latest release directly over the network.

    + +

    Test ISO Info

    We provide daily snapshot ISOs. Those are largely untested, @@ -57,8 +65,7 @@

    Test ISO Info

  • Download snapshots
  • Boot latest snapshots over the network - New
  • + title="Arch Linux Netboot Live System">Boot latest snapshots over the network
  • Feedback
  • -- cgit v1.2.3-54-g00ecf From dfad3c04e905c0058e6acdbdb229c8cc4ed78cd4 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 7 Aug 2012 23:19:42 -0500 Subject: Bump some virtualenv requirements Signed-off-by: Dan McGee --- requirements.txt | 4 ++-- requirements_prod.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index db6fcfa4..c5a37e3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Django==1.4.1 Markdown==2.2.0 -South==0.7.5 +South==0.7.6 django-countries==1.2 pgpdump==1.3 -pytz>=2012c +pytz>=2012d diff --git a/requirements_prod.txt b/requirements_prod.txt index 1c78b9fe..a31bf311 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -1,6 +1,6 @@ Django==1.4.1 Markdown==2.2.0 -South==0.7.5 +South==0.7.6 django-countries==1.2 pgpdump==1.3 psycopg2==2.4.5 -- cgit v1.2.3-54-g00ecf From b7a03d89d126989bf53005404759482e17163991 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 7 Aug 2012 23:22:18 -0500 Subject: Push more default values down into the database This makes it easier to do manual manipulation/insertion/etc. at the database level, as well as just making things act more sane from an overall software stack perspective. Signed-off-by: Dan McGee --- main/migrations/0032_auto__add_field_arch_agnostic.py | 2 +- main/migrations/0037_auto__add_field_userprofile_time_zone.py | 2 +- main/migrations/0043_auto__add_field_package_epoch.py | 2 +- main/migrations/0046_auto__add_field_repo_staging.py | 2 +- main/migrations/0048_auto__add_field_repo_bugs_category.py | 2 +- mirrors/migrations/0004_auto__add_field_mirrorprotocol_is_download.py | 2 +- ...uto__add_field_mirrorurl_has_ipv4__add_field_mirrorurl_has_ipv6.py | 4 ++-- mirrors/migrations/0010_auto__add_field_mirrorprotocol_default.py | 2 +- mirrors/migrations/0017_auto__chg_field_mirrorlog_error.py | 2 +- packages/models.py | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/main/migrations/0032_auto__add_field_arch_agnostic.py b/main/migrations/0032_auto__add_field_arch_agnostic.py index ab9b9159..9ccf059d 100644 --- a/main/migrations/0032_auto__add_field_arch_agnostic.py +++ b/main/migrations/0032_auto__add_field_arch_agnostic.py @@ -8,7 +8,7 @@ class Migration(SchemaMigration): def forwards(self, orm): # Adding field 'Arch.agnostic' - db.add_column('arches', 'agnostic', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False) + db.add_column('arches', 'agnostic', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=True) def backwards(self, orm): # Deleting field 'Arch.agnostic' diff --git a/main/migrations/0037_auto__add_field_userprofile_time_zone.py b/main/migrations/0037_auto__add_field_userprofile_time_zone.py index 9b9b8beb..3a65eacc 100644 --- a/main/migrations/0037_auto__add_field_userprofile_time_zone.py +++ b/main/migrations/0037_auto__add_field_userprofile_time_zone.py @@ -8,7 +8,7 @@ class Migration(SchemaMigration): def forwards(self, orm): # Adding field 'UserProfile.time_zone' - db.add_column('user_profiles', 'time_zone', self.gf('django.db.models.fields.CharField')(default='UTC', max_length=100), keep_default=False) + db.add_column('user_profiles', 'time_zone', self.gf('django.db.models.fields.CharField')(default='UTC', max_length=100), keep_default=True) def backwards(self, orm): # Deleting field 'UserProfile.time_zone' diff --git a/main/migrations/0043_auto__add_field_package_epoch.py b/main/migrations/0043_auto__add_field_package_epoch.py index 77cd9b49..1c6ae9db 100644 --- a/main/migrations/0043_auto__add_field_package_epoch.py +++ b/main/migrations/0043_auto__add_field_package_epoch.py @@ -9,7 +9,7 @@ class Migration(SchemaMigration): def forwards(self, orm): # Adding field 'Package.epoch' - db.add_column('packages', 'epoch', self.gf('django.db.models.fields.PositiveIntegerField')(default=0), keep_default=False) + db.add_column('packages', 'epoch', self.gf('django.db.models.fields.PositiveIntegerField')(default=0), keep_default=True) def backwards(self, orm): diff --git a/main/migrations/0046_auto__add_field_repo_staging.py b/main/migrations/0046_auto__add_field_repo_staging.py index 40c3cb20..0daaf69b 100644 --- a/main/migrations/0046_auto__add_field_repo_staging.py +++ b/main/migrations/0046_auto__add_field_repo_staging.py @@ -7,7 +7,7 @@ class Migration(SchemaMigration): def forwards(self, orm): - db.add_column('repos', 'staging', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False) + db.add_column('repos', 'staging', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=True) def backwards(self, orm): db.delete_column('repos', 'staging') diff --git a/main/migrations/0048_auto__add_field_repo_bugs_category.py b/main/migrations/0048_auto__add_field_repo_bugs_category.py index 30575126..3e61f7ed 100644 --- a/main/migrations/0048_auto__add_field_repo_bugs_category.py +++ b/main/migrations/0048_auto__add_field_repo_bugs_category.py @@ -7,7 +7,7 @@ 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) + db.add_column('repos', 'bugs_category', self.gf('django.db.models.fields.SmallIntegerField')(default=2), keep_default=False) def backwards(self, orm): db.delete_column('repos', 'bugs_category') diff --git a/mirrors/migrations/0004_auto__add_field_mirrorprotocol_is_download.py b/mirrors/migrations/0004_auto__add_field_mirrorprotocol_is_download.py index 5e44d211..0506e2cd 100644 --- a/mirrors/migrations/0004_auto__add_field_mirrorprotocol_is_download.py +++ b/mirrors/migrations/0004_auto__add_field_mirrorprotocol_is_download.py @@ -7,7 +7,7 @@ class Migration(SchemaMigration): def forwards(self, orm): - db.add_column('mirrors_mirrorprotocol', 'is_download', self.gf('django.db.models.fields.BooleanField')(default=True), keep_default=False) + db.add_column('mirrors_mirrorprotocol', 'is_download', self.gf('django.db.models.fields.BooleanField')(default=True), keep_default=True) def backwards(self, orm): db.delete_column('mirrors_mirrorprotocol', 'is_download') diff --git a/mirrors/migrations/0006_auto__add_field_mirrorurl_has_ipv4__add_field_mirrorurl_has_ipv6.py b/mirrors/migrations/0006_auto__add_field_mirrorurl_has_ipv4__add_field_mirrorurl_has_ipv6.py index a5e34589..5a40207d 100644 --- a/mirrors/migrations/0006_auto__add_field_mirrorurl_has_ipv4__add_field_mirrorurl_has_ipv6.py +++ b/mirrors/migrations/0006_auto__add_field_mirrorurl_has_ipv4__add_field_mirrorurl_has_ipv6.py @@ -7,8 +7,8 @@ class Migration(SchemaMigration): def forwards(self, orm): - db.add_column('mirrors_mirrorurl', 'has_ipv4', self.gf('django.db.models.fields.BooleanField')(default=True), keep_default=False) - db.add_column('mirrors_mirrorurl', 'has_ipv6', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False) + db.add_column('mirrors_mirrorurl', 'has_ipv4', self.gf('django.db.models.fields.BooleanField')(default=True), keep_default=True) + db.add_column('mirrors_mirrorurl', 'has_ipv6', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=True) def backwards(self, orm): db.delete_column('mirrors_mirrorurl', 'has_ipv4') diff --git a/mirrors/migrations/0010_auto__add_field_mirrorprotocol_default.py b/mirrors/migrations/0010_auto__add_field_mirrorprotocol_default.py index d30c78c7..66e60090 100644 --- a/mirrors/migrations/0010_auto__add_field_mirrorprotocol_default.py +++ b/mirrors/migrations/0010_auto__add_field_mirrorprotocol_default.py @@ -6,7 +6,7 @@ class Migration(SchemaMigration): def forwards(self, orm): - db.add_column('mirrors_mirrorprotocol', 'default', self.gf('django.db.models.fields.BooleanField')(default=True), keep_default=False) + db.add_column('mirrors_mirrorprotocol', 'default', self.gf('django.db.models.fields.BooleanField')(default=True), keep_default=True) def backwards(self, orm): db.delete_column('mirrors_mirrorprotocol', 'default') diff --git a/mirrors/migrations/0017_auto__chg_field_mirrorlog_error.py b/mirrors/migrations/0017_auto__chg_field_mirrorlog_error.py index 2f76c099..60c4ec26 100644 --- a/mirrors/migrations/0017_auto__chg_field_mirrorlog_error.py +++ b/mirrors/migrations/0017_auto__chg_field_mirrorlog_error.py @@ -7,7 +7,7 @@ class Migration(SchemaMigration): def forwards(self, orm): - db.alter_column('mirrors_mirrorlog', 'error', self.gf('django.db.models.fields.TextField')()) + db.alter_column('mirrors_mirrorlog', 'error', self.gf('django.db.models.fields.TextField')(default='')) def backwards(self, orm): db.alter_column('mirrors_mirrorlog', 'error', self.gf('django.db.models.fields.CharField')(max_length=255)) diff --git a/packages/models.py b/packages/models.py index 403dfef0..11fd0a66 100644 --- a/packages/models.py +++ b/packages/models.py @@ -176,7 +176,7 @@ class FlagRequest(models.Model): ip_address = models.GenericIPAddressField(verbose_name='IP address', unpack_ipv4=True) pkgbase = models.CharField(max_length=255, db_index=True) - version = models.CharField(max_length=255, default='') + version = models.CharField(max_length=255) repo = models.ForeignKey(Repo) num_packages = models.PositiveIntegerField('number of packages', default=1) message = models.TextField('message to developer', blank=True) -- cgit v1.2.3-54-g00ecf From 064a1b7699ef8f65ee1c123f93cd8da02e843dea Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 8 Aug 2012 19:12:06 -0500 Subject: Fix alpm ctypes interface on systems not having alpm Signed-off-by: Dan McGee --- packages/alpm.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/alpm.py b/packages/alpm.py index a7f4c3b5..3762ea68 100644 --- a/packages/alpm.py +++ b/packages/alpm.py @@ -5,16 +5,23 @@ def load_alpm(name=None): # Load the alpm library and set up some of the functions we might use - if name == None: + if name is None: name = find_library('alpm') + if name is None: + # couldn't locate the correct library + return None try: alpm = ctypes.cdll.LoadLibrary(name) except OSError: return None - alpm.alpm_version.argtypes = () - alpm.alpm_version.restype = ctypes.c_char_p - alpm.alpm_pkg_vercmp.argtypes = (ctypes.c_char_p, ctypes.c_char_p) - alpm.alpm_pkg_vercmp.restype = ctypes.c_int + try: + alpm.alpm_version.argtypes = () + alpm.alpm_version.restype = ctypes.c_char_p + alpm.alpm_pkg_vercmp.argtypes = (ctypes.c_char_p, ctypes.c_char_p) + alpm.alpm_pkg_vercmp.restype = ctypes.c_int + except AttributeError: + return None + return alpm -- cgit v1.2.3-54-g00ecf From 978a5c61a5412eeed054307d3e2979324ffcb64a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 8 Aug 2012 19:34:37 -0500 Subject: Add flag requests to developer last action calculation Signed-off-by: Dan McGee --- devel/views.py | 8 +++++++- templates/devel/clock.html | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/devel/views.py b/devel/views.py index f877bc84..ad1f4deb 100644 --- a/devel/views.py +++ b/devel/views.py @@ -29,7 +29,7 @@ from main.models import Package, PackageFile, TodolistPkg from main.models import Arch, Repo from news.models import News -from packages.models import PackageRelation, Signoff, Depend +from packages.models import PackageRelation, Signoff, FlagRequest, Depend from packages.utils import get_signoff_groups from todolists.utils import get_annotated_todolists from .utils import get_annotated_maintainers, UserFinder @@ -103,6 +103,11 @@ def clock(request): latest_signoff = dict(Signoff.objects.filter( user__is_active=True).values_list('user').order_by( ).annotate(last_signoff=Max('created'))) + # The extra() bit ensures we can use our 'user_id IS NOT NULL' index + latest_flagreq = dict(FlagRequest.objects.filter( + user__is_active=True).extra( + where=['user_id IS NOT NULL']).values_list('user_id').order_by( + ).annotate(last_flagrequest=Max('created'))) latest_log = dict(LogEntry.objects.filter( user__is_active=True).values_list('user').order_by( ).annotate(last_log=Max('action_time'))) @@ -112,6 +117,7 @@ def clock(request): latest_news.get(dev.id, None), latest_package.get(dev.id, None), latest_signoff.get(dev.id, None), + latest_flagreq.get(dev.id, None), latest_log.get(dev.id, None), dev.last_login, ] diff --git a/templates/devel/clock.html b/templates/devel/clock.html index bf4614b5..02e42749 100644 --- a/templates/devel/clock.html +++ b/templates/devel/clock.html @@ -20,6 +20,7 @@

    Developer World Clocks

    mirror)
  • Post date of a news item
  • Signing off on a package
  • +
  • Flagging a package out-of-date
  • Current UTC Time: {{ utc_now|date:"Y-m-d H:i T" }} -- cgit v1.2.3-54-g00ecf From 411ccfb3c74c521969ca6b68459289134976547d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 8 Aug 2012 22:04:07 -0500 Subject: Begin split of flag request version column into parts Not sure why on only this one I decided to put all three parts in the same column. We don't do this anywhere else. Signed-off-by: Dan McGee --- .../0023_split_flag_req_version_field.py | 222 +++++++++++++++++++++ packages/models.py | 14 ++ 2 files changed, 236 insertions(+) create mode 100644 packages/migrations/0023_split_flag_req_version_field.py diff --git a/packages/migrations/0023_split_flag_req_version_field.py b/packages/migrations/0023_split_flag_req_version_field.py new file mode 100644 index 00000000..b3d6c05c --- /dev/null +++ b/packages/migrations/0023_split_flag_req_version_field.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +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_flagrequest', 'pkgver', + self.gf('django.db.models.fields.CharField')(default='', max_length=255), + keep_default=False) + db.add_column('packages_flagrequest', 'pkgrel', + self.gf('django.db.models.fields.CharField')(default='', max_length=255), + keep_default=False) + db.add_column('packages_flagrequest', 'epoch', + self.gf('django.db.models.fields.PositiveIntegerField')(default=0), + keep_default=True) + + def backwards(self, orm): + db.delete_column('packages_flagrequest', 'pkgver') + db.delete_column('packages_flagrequest', 'pkgrel') + db.delete_column('packages_flagrequest', 'epoch') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'main.arch': { + 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"}, + 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'main.package': { + 'Meta': {'ordering': "('pkgname',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + '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': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'", 'on_delete': 'models.PROTECT', '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': '2'}), + '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.depend': { + 'Meta': {'ordering': "['name']", 'object_name': 'Depend'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'deptype': ('django.db.models.fields.CharField', [], {'default': "'D'", 'max_length': '1'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + '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': "'depends'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.flagrequest': { + 'Meta': {'object_name': 'FlagRequest'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'is_legitimate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'num_packages': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'version': ('django.db.models.fields.CharField', [], {'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', 'db_index': 'True'}), + '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'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'packages.update': { + 'Meta': {'object_name': 'Update'}, + 'action_flag': ('django.db.models.fields.PositiveSmallIntegerField', [], {}), + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Arch']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'new_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'new_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'old_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Package']"}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Repo']"}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index 11fd0a66..03da27ec 100644 --- a/packages/models.py +++ b/packages/models.py @@ -177,6 +177,9 @@ class FlagRequest(models.Model): unpack_ipv4=True) pkgbase = models.CharField(max_length=255, db_index=True) version = models.CharField(max_length=255) + pkgver = models.CharField(max_length=255) + pkgrel = models.CharField(max_length=255) + epoch = models.PositiveIntegerField(default=0) repo = models.ForeignKey(Repo) num_packages = models.PositiveIntegerField('number of packages', default=1) message = models.TextField('message to developer', blank=True) @@ -193,6 +196,17 @@ def who(self): return self.user.get_full_name() return self.user_email + @property + def full_version(self): + # Difference here from other implementations at the moment: we need to + # handle the case of pkgver and pkgrel being null as this table didn't + # originally have version columns. + if self.pkgver == '' and self.pkgrel == '': + return u'' + if self.epoch > 0: + return u'%d:%s-%s' % (self.epoch, self.pkgver, self.pkgrel) + return u'%s-%s' % (self.pkgver, self.pkgrel) + def __unicode__(self): return u'%s from %s on %s' % (self.pkgbase, self.who(), self.created) -- cgit v1.2.3-54-g00ecf From 241ff8fbd79f9f17cd326a34eb39096851f630ba Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 8 Aug 2012 22:07:06 -0500 Subject: Extract parse_version function from reporead logic Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 9 ++------- packages/utils.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index 8b55b09a..af0a2dc0 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -34,6 +34,7 @@ from main.models import Arch, Package, PackageFile, Repo from main.utils import database_vendor from packages.models import Depend, Conflict, Provision, Replacement, Update +from packages.utils import parse_version logging.basicConfig( @@ -84,8 +85,6 @@ class RepoPackage(object): 'conflicts', 'provides', 'replaces', 'groups', 'license', 'files' ) - version_re = re.compile(r'^((\d+):)?(.+)-([^-]+)$') - def __init__(self, repo): self.repo = repo self.ver = None @@ -112,11 +111,7 @@ def populate(self, values): # do NOT prune these values at all setattr(self, k, v[0]) elif k == 'version': - match = self.version_re.match(v[0]) - self.ver = match.group(3) - self.rel = match.group(4) - if match.group(2): - self.epoch = int(match.group(2)) + self.ver, self.rel, self.epoch = parse_version(v[0]) elif k == 'builddate': try: builddate = datetime.utcfromtimestamp(int(v[0])) diff --git a/packages/utils.py b/packages/utils.py index 6d54d71a..d4b4e611 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -1,6 +1,7 @@ from collections import defaultdict from itertools import chain from operator import itemgetter +import re from django.core.serializers.json import DjangoJSONEncoder from django.db import connection @@ -14,6 +15,23 @@ License, Depend, Conflict, Provision, Replacement, SignoffSpecification, Signoff, DEFAULT_SIGNOFF_SPEC) + +VERSION_RE = re.compile(r'^((\d+):)?(.+)-([^-]+)$') + + +def parse_version(version): + match = VERSION_RE.match(version) + if not match: + return None, None, 0 + ver = match.group(3) + rel = match.group(4) + if match.group(2): + epoch = int(match.group(2)) + else: + epoch = 0 + return ver, rel, epoch + + @cache_function(127) def get_group_info(include_arches=None): raw_groups = PackageGroup.objects.values_list( -- cgit v1.2.3-54-g00ecf From ad05f3eb2c8511c63dbdc9378bf3561ab949e940 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 8 Aug 2012 22:07:38 -0500 Subject: PEP8 cleanups in package utils Signed-off-by: Dan McGee --- packages/utils.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/utils.py b/packages/utils.py index d4b4e611..d95c015f 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -36,7 +36,7 @@ def parse_version(version): def get_group_info(include_arches=None): raw_groups = PackageGroup.objects.values_list( 'name', 'pkg__arch__name').order_by('name').annotate( - cnt=Count('pkg'), last_update=Max('pkg__last_update')) + cnt=Count('pkg'), last_update=Max('pkg__last_update')) # now for post_processing. we need to seperate things out and add # the count in for 'any' to all of the other architectures. group_mapping = {} @@ -71,6 +71,7 @@ def get_group_info(include_arches=None): groups.extend(val.itervalues()) return sorted(groups, key=itemgetter('name', 'arch')) + def get_split_packages_info(): '''Return info on split packages that do not have an actual package name matching the split pkgbase.''' @@ -276,6 +277,7 @@ def approved_by_signoffs(signoffs, spec): return good_signoffs >= spec.required return False + class PackageSignoffGroup(object): '''Encompasses all packages in testing with the same pkgbase.''' def __init__(self, packages): @@ -375,6 +377,7 @@ def __unicode__(self): AND p.repo_id IN (%s) """ + def get_current_signoffs(repos): '''Returns a mapping of pkgbase -> signoff objects for the given repos.''' cursor = connection.cursor() @@ -389,6 +392,7 @@ def get_current_signoffs(repos): signoffs = Signoff.objects.select_related('user').in_bulk(to_fetch) return signoffs.values() + def get_current_specifications(repos): '''Returns a mapping of pkgbase -> signoff specification objects for the given repos.''' @@ -401,6 +405,7 @@ def get_current_specifications(repos): to_fetch = [row[0] for row in results] return SignoffSpecification.objects.in_bulk(to_fetch).values() + def get_target_repo_map(repos): sql = """ SELECT DISTINCT p1.pkgbase, r.name @@ -421,6 +426,7 @@ def get_target_repo_map(repos): cursor.execute(sql, params) return dict(cursor.fetchall()) + def get_signoff_groups(repos=None, user=None): if repos is None: repos = Repo.objects.filter(testing=True) @@ -458,12 +464,12 @@ def get_signoff_groups(repos=None, user=None): class PackageJSONEncoder(DjangoJSONEncoder): - pkg_attributes = [ 'pkgname', 'pkgbase', 'repo', 'arch', 'pkgver', + pkg_attributes = ['pkgname', 'pkgbase', 'repo', 'arch', 'pkgver', 'pkgrel', 'epoch', 'pkgdesc', 'url', 'filename', 'compressed_size', 'installed_size', 'build_date', 'last_update', 'flag_date', - 'maintainers', 'packager' ] - pkg_list_attributes = [ 'groups', 'licenses', 'conflicts', - 'provides', 'replaces', 'depends' ] + 'maintainers', 'packager'] + pkg_list_attributes = ['groups', 'licenses', 'conflicts', + 'provides', 'replaces', 'depends'] def default(self, obj): if hasattr(obj, '__iter__'): @@ -488,5 +494,4 @@ def default(self, obj): return obj.username return super(PackageJSONEncoder, self).default(obj) - # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From b425b192e12afd0584bbffc9ff1d997a330bcd5a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 8 Aug 2012 22:21:05 -0500 Subject: Migrate flag request version info to new format Signed-off-by: Dan McGee --- main/models.py | 8 +- packages/admin.py | 4 +- .../migrations/0024_move_flag_req_version_info.py | 218 +++++++++++++++++++++ packages/views/flag.py | 10 +- 4 files changed, 232 insertions(+), 8 deletions(-) create mode 100644 packages/migrations/0024_move_flag_req_version_info.py diff --git a/main/models.py b/main/models.py index bb2320e8..95e3c42b 100644 --- a/main/models.py +++ b/main/models.py @@ -345,12 +345,16 @@ def split_packages(self): pkgbase=self.pkgbase).exclude(id=self.id) def flag_request(self): - if not self.flag_date: + if self.flag_date is None: return None from packages.models import FlagRequest try: + # Note that we don't match on pkgrel here; this is because a pkgrel + # bump does not unflag a package so we can still show the same flag + # request from a different pkgrel. request = FlagRequest.objects.filter(pkgbase=self.pkgbase, - repo=self.repo).latest() + repo=self.repo, pkgver=self.pkgver, + epoch=self.epoch).latest() return request except FlagRequest.DoesNotExist: return None diff --git a/packages/admin.py b/packages/admin.py index 51c6fb06..5e32dbb4 100644 --- a/packages/admin.py +++ b/packages/admin.py @@ -12,8 +12,8 @@ class PackageRelationAdmin(admin.ModelAdmin): class FlagRequestAdmin(admin.ModelAdmin): - list_display = ('pkgbase', 'version', 'repo', 'created', 'who', 'is_spam', - 'is_legitimate', 'message') + list_display = ('pkgbase', 'full_version', 'repo', 'created', 'who', + 'is_spam', 'is_legitimate', 'message') list_filter = ('is_spam', 'is_legitimate', 'repo') search_fields = ('pkgbase', 'user_email', 'message') ordering = ('-created',) diff --git a/packages/migrations/0024_move_flag_req_version_info.py b/packages/migrations/0024_move_flag_req_version_info.py new file mode 100644 index 00000000..3be4654a --- /dev/null +++ b/packages/migrations/0024_move_flag_req_version_info.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import DataMigration +from django.db import models + +from packages.utils import parse_version + + +class Migration(DataMigration): + + def forwards(self, orm): + for pk, version in orm.FlagRequest.objects.exclude( + version='').values_list('pk', 'version'): + ver, rel, epoch = parse_version(version) + orm.FlagRequest.objects.filter(pk=pk).update( + pkgver=ver, pkgrel=rel, epoch=epoch) + + def backwards(self, orm): + orm.FlagRequest.objects.all().update(pkgver='', pkgrel='', epoch=0) + + 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',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + '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': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'", 'on_delete': 'models.PROTECT', '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': '2'}), + '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.depend': { + 'Meta': {'ordering': "['name']", 'object_name': 'Depend'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'deptype': ('django.db.models.fields.CharField', [], {'default': "'D'", 'max_length': '1'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + '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': "'depends'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.flagrequest': { + 'Meta': {'object_name': 'FlagRequest'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'is_legitimate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'num_packages': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'version': ('django.db.models.fields.CharField', [], {'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', 'db_index': 'True'}), + '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'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'packages.update': { + 'Meta': {'object_name': 'Update'}, + 'action_flag': ('django.db.models.fields.PositiveSmallIntegerField', [], {}), + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Arch']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'new_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'new_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'old_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Package']"}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Repo']"}) + } + } + + complete_apps = ['packages'] + symmetrical = True diff --git a/packages/views/flag.py b/packages/views/flag.py index b9542a62..16f5f202 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -59,11 +59,12 @@ def flag(request, name, repo, arch): flagged_pkgs = list(pkgs) # find a common version if there is one available to store - versions = set(pkg.full_version for pkg in flagged_pkgs) + versions = set((pkg.pkgver, pkg.pkgrel, pkg.epoch) + for pkg in flagged_pkgs) if len(versions) == 1: version = versions.pop() else: - version = '' + version = ('', '', 0) message = form.cleaned_data['message'] ip_addr = request.META.get('REMOTE_ADDR') @@ -77,11 +78,12 @@ def perform_updates(): current_time = now() pkgs.update(flag_date=current_time) # store our flag request + # TODO flag_request = FlagRequest(created=current_time, user_email=email, message=message, ip_address=ip_addr, pkgbase=pkg.pkgbase, - version=version, repo=pkg.repo, - num_packages=len(flagged_pkgs)) + repo=pkg.repo, pkgver=version[0], pkgrel=version[1], + epoch=version[2], num_packages=len(flagged_pkgs)) if authenticated: flag_request.user = request.user flag_request.save() -- cgit v1.2.3-54-g00ecf From 32e3e24bbc26a44cbf6fce06cf802ee26f81aa48 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 8 Aug 2012 22:22:24 -0500 Subject: Drop old flag request version column Signed-off-by: Dan McGee --- .../0025_auto__del_field_flagrequest_version.py | 213 +++++++++++++++++++++ packages/models.py | 1 - 2 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 packages/migrations/0025_auto__del_field_flagrequest_version.py diff --git a/packages/migrations/0025_auto__del_field_flagrequest_version.py b/packages/migrations/0025_auto__del_field_flagrequest_version.py new file mode 100644 index 00000000..963b0b12 --- /dev/null +++ b/packages/migrations/0025_auto__del_field_flagrequest_version.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.delete_column('packages_flagrequest', 'version') + + def backwards(self, orm): + db.add_column('packages_flagrequest', 'version', + self.gf('django.db.models.fields.CharField')(default='', max_length=255), + keep_default=False) + + + 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',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + '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': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'", 'on_delete': 'models.PROTECT', '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': '2'}), + '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.depend': { + 'Meta': {'ordering': "['name']", 'object_name': 'Depend'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'deptype': ('django.db.models.fields.CharField', [], {'default': "'D'", 'max_length': '1'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + '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': "'depends'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.flagrequest': { + 'Meta': {'object_name': 'FlagRequest'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'is_legitimate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'num_packages': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}) + }, + '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', 'db_index': 'True'}), + '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'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('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', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'packages.update': { + 'Meta': {'object_name': 'Update'}, + 'action_flag': ('django.db.models.fields.PositiveSmallIntegerField', [], {}), + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Arch']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'new_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'new_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_epoch': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'old_pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'old_pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Package']"}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updates'", 'to': "orm['main.Repo']"}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index 03da27ec..af428a77 100644 --- a/packages/models.py +++ b/packages/models.py @@ -176,7 +176,6 @@ class FlagRequest(models.Model): ip_address = models.GenericIPAddressField(verbose_name='IP address', unpack_ipv4=True) pkgbase = models.CharField(max_length=255, db_index=True) - version = models.CharField(max_length=255) pkgver = models.CharField(max_length=255) pkgrel = models.CharField(max_length=255) epoch = models.PositiveIntegerField(default=0) -- cgit v1.2.3-54-g00ecf From 5549b119ea84ffd60f2987610ae35fb393d9625e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 9 Aug 2012 20:31:16 -0500 Subject: Add a rate limiting filter for log messages This should help cut down on the massive amount of emails I receive when things go wrong on the production website. Signed-off-by: Dan McGee --- main/log.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 main/log.py diff --git a/main/log.py b/main/log.py new file mode 100644 index 00000000..63634874 --- /dev/null +++ b/main/log.py @@ -0,0 +1,71 @@ +# Derived from Django snippets: http://djangosnippets.org/snippets/2242/ +from collections import OrderedDict +from datetime import datetime, timedelta +from hashlib import md5 +import traceback +from pytz import utc + + +class LimitedSizeDict(OrderedDict): + def __init__(self, *args, **kwargs): + self.size_limit = kwargs.pop('size', None) + if self.size_limit == 0: + self.size_limit = None + if self.size_limit and self.size_limit < 0: + raise Exception('Invalid size specified') + super(LimitedSizeDict, self).__init__(*args, **kwargs) + self.check_item_limits() + + def __setitem__(self, key, value): + # delete and add to ensure it ends up at the end of the linked list + if key in self: + super(LimitedSizeDict, self).__delitem__(key) + super(LimitedSizeDict, self).__setitem__(key, value) + self.check_item_limits() + + def check_item_limits(self): + if self.size_limit is None: + return + while len(self) > self.size_limit: + self.popitem(last=False) + + +class RateLimitFilter(object): + def __init__(self, name='', rate=10, prefix='error_rate', max_keys=100): + # delayed import otherwise we have a circular dep when setting up + # the logging config: settings -> logging -> cache -> settings + self.cache_module = __import__('django.core.cache', fromlist=['cache']) + self.errors = LimitedSizeDict(size=max_keys) + self.rate = rate + self.prefix = prefix + + def filter(self, record): + if self.rate == 0: + # rate == 0 means totally unfiltered + return True + + trace = '\n'.join(traceback.format_exception(*record.exc_info)) + key = md5(trace).hexdigest() + duplicate = False + cache = self.cache_module.cache + + # Test if the cache works + try: + cache.set(self.prefix, 1, 300) + use_cache = (cache.get(self.prefix) == 1) + except: + use_cache = False + + if use_cache: + cache_key = '%s_%s' % (self.prefix, key) + duplicate = (cache.get(cache_key) == 1) + cache.set(cache_key, 1, self.rate) + else: + now = datetime.utcnow().replace(tzinfo=utc) + min_date = now - timedelta(seconds=self.rate) + duplicate = (key in self.errors and self.errors[key] >= min_date) + self.errors[key] = now + + return not duplicate + +# vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From aa7f51e6bd80fded21fabf3135ebe78ff32b24c7 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 13 Aug 2012 09:38:47 -0500 Subject: Enable rate-limiting log filter Signed-off-by: Dan McGee --- settings.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/settings.py b/settings.py index 8b74e554..80df6f43 100644 --- a/settings.py +++ b/settings.py @@ -78,6 +78,7 @@ 'django.middleware.doc.XViewMiddleware', ) +# Base of the URL hierarchy ROOT_URLCONF = 'urls' # URL to serve static files @@ -123,6 +124,31 @@ 'retro', ) +# Logging configuration for not getting overspammed +LOGGING = { + 'version': 1, + 'filters': { + 'ratelimit': { + '()': 'main.log.RateLimitFilter', + } + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['ratelimit'], + 'class': 'django.utils.log.AdminEmailHandler', + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + }, +} + + ## Server used for linking to PGP keysearch results PGP_SERVER = 'pgp.mit.edu:11371' -- cgit v1.2.3-54-g00ecf From 3cb16e4784f492c50555e879ea6b07fd898b1c3d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 13 Aug 2012 09:34:11 -0500 Subject: Attempt to screen for useless out-of-date messages Things like ' ', '-', '.', etc. will no longer be accepted in this field. Signed-off-by: Dan McGee --- packages/views/flag.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/views/flag.py b/packages/views/flag.py index 16f5f202..33cec006 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -1,3 +1,5 @@ +import re + from django import forms from django.conf import settings from django.contrib.auth.decorators import permission_required @@ -30,6 +32,15 @@ def __init__(self, *args, **kwargs): if auth: del self.fields['email'] + def clean_message(self): + data = self.cleaned_data['message'] + # make sure the message isn't garbage (only punctuation or whitespace) + # and ensure a certain minimum length + if re.match(r'^[^0-9A-Za-z]+$', data) or len(data) < 3: + raise forms.ValidationError( + "Enter a valid and useful out-of-date message.") + return data + @cache_page(3600) def flaghelp(request): @@ -78,7 +89,6 @@ def perform_updates(): current_time = now() pkgs.update(flag_date=current_time) # store our flag request - # TODO flag_request = FlagRequest(created=current_time, user_email=email, message=message, ip_address=ip_addr, pkgbase=pkg.pkgbase, -- cgit v1.2.3-54-g00ecf From bc5dab41a82ff980c51dd4e408983e32886721bb Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 13 Aug 2012 10:11:20 -0500 Subject: releng views code cleanup Signed-off-by: Dan McGee --- releng/views.py | 54 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/releng/views.py b/releng/views.py index a1bc3b81..67b3cb4a 100644 --- a/releng/views.py +++ b/releng/views.py @@ -8,11 +8,13 @@ Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source, Test) + def standard_field(model, empty_label=None, help_text=None, required=True): return forms.ModelChoiceField(queryset=model.objects.all(), widget=forms.RadioSelect(), empty_label=empty_label, help_text=help_text, required=required) + class TestForm(forms.ModelForm): iso = forms.ModelChoiceField(queryset=Iso.objects.filter( active=True).order_by('-id')) @@ -24,29 +26,34 @@ class TestForm(forms.ModelForm): source = standard_field(Source) clock_choice = standard_field(ClockChoice) filesystem = standard_field(Filesystem, - help_text="Verify /etc/fstab, `df -hT` output and commands like " \ + 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) + widget=forms.CheckboxSelectMultiple(), required=False) bootloader = standard_field(Bootloader, - help_text="Verify that the entries in the bootloader config looks OK.") + help_text="Verify that the entries in the bootloader config " + "looks 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.", + 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.", + 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 ran into problems please create a ticket on the " \ - "bugtracker (or check that one already exists) and link to " \ + help_text="Only check this if everything went fine. " + "If you ran into problems please create a ticket on the " + "bugtracker (or check that one already exists) and link to " "it in the comments.", required=False) website = forms.CharField(label='', - widget=forms.TextInput(attrs={'style': 'display:none;'}), required=False) + widget=forms.TextInput(attrs={'style': 'display:none;'}), + required=False) class Meta: model = Test @@ -59,6 +66,7 @@ class Meta: "modules": forms.CheckboxSelectMultiple(), } + def submit_test_result(request): if request.POST: form = TestForm(request.POST) @@ -74,6 +82,7 @@ def submit_test_result(request): context = {'form': form} return render(request, 'releng/add.html', context) + def calculate_option_overview(field_name): field = Test._meta.get_field(field_name) model = field.rel.to @@ -108,6 +117,7 @@ def calculate_option_overview(field_name): return option + def options_fetch_iso(options): '''Replaces the Iso PK with a full Iso model object in a list of options used on the overview page. We do it this way to only have to query the Iso @@ -128,13 +138,19 @@ def options_fetch_iso(options): return options + def test_results_overview(request): # data structure produced: - # [ { option, name, is_rollback, values: [ { value, success, failure } ... ] } ... ] + # [ { + # option, name, is_rollback, + # values: [ { value, success, failure } ... ] + # } + # ... + # ] all_options = [] - fields = [ 'architecture', 'iso_type', 'boot_type', 'hardware_type', + fields = ['architecture', 'iso_type', 'boot_type', 'hardware_type', 'install_type', 'source', 'clock_choice', 'filesystem', 'modules', - 'bootloader', 'rollback_filesystem', 'rollback_modules' ] + 'bootloader', 'rollback_filesystem', 'rollback_modules'] for field in fields: all_options.append(calculate_option_overview(field)) @@ -146,6 +162,7 @@ def test_results_overview(request): } return render(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.select_related() @@ -155,6 +172,7 @@ def test_results_iso(request, iso_id): } return render(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 @@ -171,9 +189,11 @@ def test_results_for(request, option, value): } return render(request, 'releng/result_list.html', context) + def submit_test_thanks(request): return render(request, "releng/thanks.html", None) + def iso_overview(request): isos = Iso.objects.all().order_by('-pk') successes = dict(Iso.objects.values_list('pk').filter( @@ -186,7 +206,7 @@ def iso_overview(request): # only show "useful" rows, currently active ISOs or those with results isos = [iso for iso in isos if - iso.active == True or iso.successes > 0 or iso.failures > 0] + iso.active is True or iso.successes > 0 or iso.failures > 0] context = { 'isos': isos -- cgit v1.2.3-54-g00ecf From ca0011c585ec28f9dde0f400a77fd6f859d520b0 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 15 Aug 2012 08:11:00 -0500 Subject: Add ability to rematch developers on @archlinux.org addresses This makes this matcher catch a bit more with the wide net we were already casting. Signed-off-by: Dan McGee --- devel/management/commands/rematch_developers.py | 4 +- devel/utils.py | 50 +++++++++++++++++-------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/devel/management/commands/rematch_developers.py b/devel/management/commands/rematch_developers.py index ab2f0f4b..2b379588 100644 --- a/devel/management/commands/rematch_developers.py +++ b/devel/management/commands/rematch_developers.py @@ -53,6 +53,7 @@ def match_packager(finder): unmatched = Package.objects.filter(packager__isnull=True).values_list( 'packager_str', flat=True).order_by().distinct() + logger.info("%d packager strings retrieved", len(unmatched)) for packager in unmatched: logger.debug("packager string %s", packager) user = finder.find(packager) @@ -71,13 +72,14 @@ def match_packager(finder): @transaction.commit_on_success def match_flagrequest(finder): - logger.info("getting all flag requests emails from unknown users") + logger.info("getting all flag request email addresses from unknown users") req_count = matched_count = 0 mapping = {} unmatched = FlagRequest.objects.filter(user__isnull=True).values_list( 'user_email', flat=True).order_by().distinct() + logger.info("%d email addresses retrieved", len(unmatched)) for user_email in unmatched: logger.debug("email %s", user_email) user = finder.find_by_email(user_email) diff --git a/devel/utils.py b/devel/utils.py index 85b4e42f..e8e3a6c4 100644 --- a/devel/utils.py +++ b/devel/utils.py @@ -1,6 +1,7 @@ import re from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db import connection from django.db.models import Count, Q @@ -44,6 +45,15 @@ def get_annotated_maintainers(): return maintainers +def ignore_does_not_exist(func): + def new_func(*args, **kwargs): + try: + return func(*args, **kwargs) + except (ObjectDoesNotExist, MultipleObjectsReturned): + return None + return new_func + + class UserFinder(object): def __init__(self): self.cache = {} @@ -52,18 +62,33 @@ def __init__(self): self.pgp_cache = {} @staticmethod + @ignore_does_not_exist def user_email(name, email): if email: return User.objects.get(email=email) return None @staticmethod + @ignore_does_not_exist + def username_email(name, email): + if email and '@' in email: + # split email addr at '@' symbol, ensure domain matches + # or is a subdomain of archlinux.org + # TODO: configurable domain/regex somewhere? + username, domain = email.split('@', 1) + if re.match(r'^(.+\.)?archlinux.org$', domain): + return User.objects.get(username=username) + return None + + @staticmethod + @ignore_does_not_exist def profile_email(name, email): if email: return User.objects.get(userprofile__public_email=email) return None @staticmethod + @ignore_does_not_exist def user_name(name, email): # yes, a bit odd but this is the easiest way since we can't always be # sure how to split the name. Ensure every 'token' appears in at least @@ -102,14 +127,12 @@ def find(self, userstring): email = matches.group(2) user = None - find_methods = (self.user_email, self.profile_email, self.user_name) + find_methods = (self.user_email, self.profile_email, + self.username_email, self.user_name) for matcher in find_methods: - try: - user = matcher(name, email) - if user != None: - break - except (User.DoesNotExist, User.MultipleObjectsReturned): - pass + user = matcher(name, email) + if user != None: + break self.cache[userstring] = user self.email_cache[email] = user @@ -135,14 +158,11 @@ def find_by_email(self, email): if email in self.email_cache: return self.email_cache[email] - user = None - try: - user = self.user_email(None, email) - except User.DoesNotExist: - try: - user = self.profile_email(None, email) - except User.DoesNotExist: - pass + user = self.user_email(None, email) + if user is None: + user = self.profile_email(None, email) + if user is None: + user = self.username_email(None, email) self.email_cache[email] = user return user -- cgit v1.2.3-54-g00ecf From a071d800c6a26d3efcdc0d32fe1adb1cde7e6f31 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 15 Aug 2012 08:22:01 -0500 Subject: Fix signoffs SQL query Although the old query returned the same results, the repos IN clause should really be a part of the WHERE, not the JOIN condition. Signed-off-by: Dan McGee --- packages/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/utils.py b/packages/utils.py index d95c015f..ee1b56b3 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -363,6 +363,7 @@ def __unicode__(self): return u'%s-%s (%s): %d' % ( self.pkgbase, self.version, self.arch, len(self.signoffs)) + _SQL_SPEC_OR_SIGNOFF = """ SELECT DISTINCT s.id FROM %s s @@ -374,7 +375,7 @@ def __unicode__(self): AND s.arch_id = p.arch_id AND s.repo_id = p.repo_id ) - AND p.repo_id IN (%s) + WHERE p.repo_id IN (%s) """ -- cgit v1.2.3-54-g00ecf From 9189665c2c64c13a59b25b981633ae9e12cfcc92 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 15 Aug 2012 08:24:10 -0500 Subject: Ensure created is set when creating flag request via admin Signed-off-by: Dan McGee --- packages/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/models.py b/packages/models.py index af428a77..0bea21b1 100644 --- a/packages/models.py +++ b/packages/models.py @@ -482,7 +482,8 @@ class Replacement(RelatedToBase): # hook up some signals -for sender in (PackageRelation, SignoffSpecification, Signoff, Update): +for sender in (FlagRequest, PackageRelation, + SignoffSpecification, Signoff, Update): pre_save.connect(set_created_field, sender=sender, dispatch_uid="packages.models") -- cgit v1.2.3-54-g00ecf From 2e9094630b5117575f899f068046c6e1021387a1 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 15 Aug 2012 08:25:00 -0500 Subject: PEP8 spacing in releng models file Signed-off-by: Dan McGee --- releng/models.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/releng/models.py b/releng/models.py index 56187766..98454e95 100644 --- a/releng/models.py +++ b/releng/models.py @@ -4,6 +4,7 @@ from main.utils import set_created_field + class IsoOption(models.Model): name = models.CharField(max_length=200) @@ -13,10 +14,12 @@ def __unicode__(self): class Meta: abstract = True + class RollbackOption(IsoOption): class Meta: abstract = True + class Iso(models.Model): name = models.CharField(max_length=255) created = models.DateTimeField(editable=False) @@ -32,37 +35,48 @@ def __unicode__(self): class Meta: verbose_name = 'ISO' + class Architecture(IsoOption): pass + class IsoType(IsoOption): class Meta: verbose_name = 'ISO type' + 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() -- cgit v1.2.3-54-g00ecf From 04066d190efddaf890d8573a3fbf4fd1706cb51f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 15 Aug 2012 20:26:23 -0500 Subject: Ensure reverse conflicts match architecture if applicable Signed-off-by: Dan McGee --- main/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/main/models.py b/main/models.py index 95e3c42b..202e7fa6 100644 --- a/main/models.py +++ b/main/models.py @@ -291,6 +291,10 @@ def reverse_conflicts(self): Returns a list of packages with conflicts against this package. """ pkgs = Package.objects.filter(conflicts__name=self.pkgname) + if not self.arch.agnostic: + # make sure we match architectures if possible + pkgs = pkgs.filter(arch__in=self.applicable_arches()) + alpm = AlpmAPI() if not alpm.available: return pkgs -- cgit v1.2.3-54-g00ecf From 0ec1af27aeb2ff94128c84ad53f09045cd9ee6e3 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 20 Aug 2012 21:18:18 -0500 Subject: Mark nullable fields as blank on package model This helps when creating test packages through the Django admin. Signed-off-by: Dan McGee --- main/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/models.py b/main/models.py index 202e7fa6..014464ff 100644 --- a/main/models.py +++ b/main/models.py @@ -105,10 +105,10 @@ class Package(models.Model): last_update = models.DateTimeField(db_index=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, blank=True, on_delete=models.SET_NULL) pgp_signature = models.TextField(null=True, blank=True) - flag_date = models.DateTimeField(null=True) + flag_date = models.DateTimeField(null=True, blank=True) objects = PackageManager() -- cgit v1.2.3-54-g00ecf From f7289625000d0f83675fc5a70650b49707338dca Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 20 Aug 2012 21:19:02 -0500 Subject: Use case-insensitive search in opensearch suggestions There is no real good reason not to do this, since our packages are lowercased by convention. Signed-off-by: Dan McGee --- packages/views/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/views/__init__.py b/packages/views/__init__.py index 038d40ac..f7952255 100644 --- a/packages/views/__init__.py +++ b/packages/views/__init__.py @@ -5,6 +5,7 @@ from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import User from django.core.cache import cache +from django.db.models import Q from django.http import HttpResponse from django.shortcuts import redirect, render from django.views.decorators.cache import cache_control @@ -47,8 +48,13 @@ def opensearch_suggest(request): hashlib.md5(search_term.encode('utf-8')).hexdigest() to_json = cache.get(cache_key, None) if to_json is None: - names = Package.objects.filter( - pkgname__startswith=search_term).values_list( + q = Q(pkgname__startswith=search_term) + lookup = search_term.lower() + if search_term != lookup: + # package names are lowercase by convention, so include that in + # search if original wasn't lowercase already + q |= Q(pkgname__startswith=lookup) + names = Package.objects.filter(q).values_list( 'pkgname', flat=True).order_by('pkgname').distinct()[:10] results = [search_term, list(names)] to_json = json.dumps(results, ensure_ascii=False) -- cgit v1.2.3-54-g00ecf From 78553abc51ba0f18e1c3bec015c9a11d4760c522 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 20 Aug 2012 23:48:44 -0500 Subject: Don't blow up when pgp signature data is '' on a package We handled None/NULL correctly, but not the empty string. Fix this corner case. Signed-off-by: Dan McGee --- main/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main/models.py b/main/models.py index 014464ff..42f8e89a 100644 --- a/main/models.py +++ b/main/models.py @@ -143,6 +143,8 @@ def signature(self): data = b64decode(self.pgp_signature) except TypeError: return None + if not data: + return None data = BinaryData(data) packets = list(data.packets()) return packets[0] -- cgit v1.2.3-54-g00ecf From cd51842ce86c44eef4e3c3d5334aca13e234151a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 20 Aug 2012 23:50:21 -0500 Subject: Fix scm_link for Unicode characters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This blew up with non-ASCII due to us trying to stuff 8-bit characters into the URL parameters where they aren't allowed. Tested with a package entered via the admin with pkgname and pkgbase set to 'αναζήτη'. Signed-off-by: Dan McGee --- packages/templatetags/package_extras.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py index 9daecd96..a0ee8bee 100644 --- a/packages/templatetags/package_extras.py +++ b/packages/templatetags/package_extras.py @@ -91,11 +91,12 @@ def packager_link(user): @register.simple_tag def scm_link(package, operation): - parts = (package.repo.svn_root, operation, package.pkgbase) - linkbase = ( - "https://projects.archlinux.org/svntogit/%s.git/%s/trunk?" - "h=packages/%s") - return linkbase % tuple(urlquote(part) for part in parts) + parts = (package.repo.svn_root, operation) + url = "https://projects.archlinux.org/svntogit/%s.git/%s/trunk" % parts + data = { + 'h': 'packages/%s' % package.pkgbase + } + return link_encode(url, data) @register.simple_tag def get_wiki_link(package): -- cgit v1.2.3-54-g00ecf From 1c416b97e6a9f3d6bf5f8747bd10ed1ca9cad68c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 21 Aug 2012 00:02:57 -0500 Subject: Revert "Fix scm_link for Unicode characters" This reverts commit cd51842ce86c44eef4e3c3d5334aca13e234151a. It turns out cgit doesn't like it if you escape the '/' in the URL parameter when viewing the log (a clear upstream bug), but we need to be able to work around this. Example: 'extra%2Fkdelibs' and 'extra/kdelibs' are handled differently. --- packages/templatetags/package_extras.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py index a0ee8bee..9daecd96 100644 --- a/packages/templatetags/package_extras.py +++ b/packages/templatetags/package_extras.py @@ -91,12 +91,11 @@ def packager_link(user): @register.simple_tag def scm_link(package, operation): - parts = (package.repo.svn_root, operation) - url = "https://projects.archlinux.org/svntogit/%s.git/%s/trunk" % parts - data = { - 'h': 'packages/%s' % package.pkgbase - } - return link_encode(url, data) + parts = (package.repo.svn_root, operation, package.pkgbase) + linkbase = ( + "https://projects.archlinux.org/svntogit/%s.git/%s/trunk?" + "h=packages/%s") + return linkbase % tuple(urlquote(part) for part in parts) @register.simple_tag def get_wiki_link(package): -- cgit v1.2.3-54-g00ecf From 90001d9ac781d07feba6120bfe64112481b9a5fb Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 21 Aug 2012 00:10:12 -0500 Subject: Style cleanups in package_extras Signed-off-by: Dan McGee --- packages/templatetags/package_extras.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py index 9daecd96..1aedf677 100644 --- a/packages/templatetags/package_extras.py +++ b/packages/templatetags/package_extras.py @@ -9,6 +9,7 @@ register = template.Library() + def link_encode(url, query): # massage the data into all utf-8 encoded strings first, so urlencode # doesn't barf at the data we pass it @@ -16,6 +17,7 @@ def link_encode(url, query): data = urlencode(query).replace('&', '&') return "%s?%s" % (url, data) + @register.filter def url_unquote(original_url): try: @@ -27,6 +29,7 @@ def url_unquote(original_url): except UnicodeError: return original_url + class BuildQueryStringNode(template.Node): def __init__(self, sortfield): self.sortfield = sortfield @@ -34,7 +37,7 @@ def __init__(self, sortfield): def render(self, context): qs = parse_qs(context['current_query']) - if qs.has_key('sort') and self.sortfield in qs['sort']: + if 'sort' in qs and self.sortfield in qs['sort']: if self.sortfield.startswith('-'): qs['sort'] = [self.sortfield[1:]] else: @@ -43,6 +46,7 @@ def render(self, context): qs['sort'] = [self.sortfield] return urlencode(qs, True).replace('&', '&') + @register.tag(name='buildsortqs') def do_buildsortqs(parser, token): try: @@ -55,15 +59,18 @@ def do_buildsortqs(parser, token): "%r tag's argument should be in quotes" % tagname) return BuildQueryStringNode(sortfield[1:-1]) + @register.simple_tag def pkg_details_link(pkg): link = '%s' return link % (pkg.get_absolute_url(), pkg.pkgname, pkg.pkgname) + @register.simple_tag def multi_pkg_details(pkgs): return ', '.join([pkg_details_link(pkg) for pkg in pkgs]) + @register.simple_tag def maintainer_link(user): if user: @@ -76,6 +83,7 @@ def maintainer_link(user): ) return '' + @register.simple_tag def packager_link(user): if user: @@ -97,6 +105,7 @@ def scm_link(package, operation): "h=packages/%s") return linkbase % tuple(urlquote(part) for part in parts) + @register.simple_tag def get_wiki_link(package): url = "https://wiki.archlinux.org/index.php/Special:Search" @@ -105,6 +114,7 @@ def get_wiki_link(package): } return link_encode(url, data) + @register.simple_tag def bugs_list(package): url = "https://bugs.archlinux.org/" @@ -115,6 +125,7 @@ def bugs_list(package): } return link_encode(url, data) + @register.simple_tag def bug_report(package): url = "https://bugs.archlinux.org/newtask" -- cgit v1.2.3-54-g00ecf From eb44404a17ae9feacdf2948a5cda958510ac2fed Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 21 Aug 2012 00:12:38 -0500 Subject: Fix scm_link in a way that doesn't make cgit barf This does what commit cd51842ce86 set out to do in a way that doesn't break cgit's not-so-hit query string parsing. Signed-off-by: Dan McGee --- packages/templatetags/package_extras.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py index 1aedf677..d87a74a5 100644 --- a/packages/templatetags/package_extras.py +++ b/packages/templatetags/package_extras.py @@ -103,7 +103,7 @@ def scm_link(package, operation): linkbase = ( "https://projects.archlinux.org/svntogit/%s.git/%s/trunk?" "h=packages/%s") - return linkbase % tuple(urlquote(part) for part in parts) + return linkbase % tuple(urlquote(part.encode('utf-8')) for part in parts) @register.simple_tag -- cgit v1.2.3-54-g00ecf From 0926cf30f4bacf7922c2f27e4f27f78f8182aee0 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 4 Sep 2012 08:22:38 -0500 Subject: Filter out spam flag requests on package details page No need to show these as a matching request. Signed-off-by: Dan McGee --- main/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/models.py b/main/models.py index 42f8e89a..9ab4b4ad 100644 --- a/main/models.py +++ b/main/models.py @@ -360,7 +360,7 @@ def flag_request(self): # request from a different pkgrel. request = FlagRequest.objects.filter(pkgbase=self.pkgbase, repo=self.repo, pkgver=self.pkgver, - epoch=self.epoch).latest() + epoch=self.epoch, is_spam=False).latest() return request except FlagRequest.DoesNotExist: return None -- cgit v1.2.3-54-g00ecf From 79b0ff49bd3319133502243ded4506fc2d56cd9e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 4 Sep 2012 08:37:09 -0500 Subject: Use GenericIPAddressField for releng test IP address field We were already using this on package flag requests, and we can support IPv6 addresses here as well with minimal hassle. Signed-off-by: Dan McGee --- .../0003_auto__chg_field_test_ip_address.py | 99 ++++++++++++++++++++++ releng/models.py | 6 +- 2 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 releng/migrations/0003_auto__chg_field_test_ip_address.py diff --git a/releng/migrations/0003_auto__chg_field_test_ip_address.py b/releng/migrations/0003_auto__chg_field_test_ip_address.py new file mode 100644 index 00000000..78297f14 --- /dev/null +++ b/releng/migrations/0003_auto__chg_field_test_ip_address.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.alter_column('releng_test', 'ip_address', self.gf('django.db.models.fields.GenericIPAddressField')(max_length=39)) + + def backwards(self, orm): + db.alter_column('releng_test', 'ip_address', self.gf('django.db.models.fields.IPAddressField')(max_length=15)) + + 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'}), + 'removed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}) + }, + '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.GenericIPAddressField', [], {'max_length': '39'}), + '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/models.py b/releng/models.py index 98454e95..d602e9e5 100644 --- a/releng/models.py +++ b/releng/models.py @@ -79,8 +79,10 @@ class Bootloader(IsoOption): class Test(models.Model): user_name = models.CharField(max_length=500) - user_email = models.EmailField() - ip_address = models.IPAddressField() + user_email = models.EmailField('email address') + # Great work, Django... https://code.djangoproject.com/ticket/18212 + ip_address = models.GenericIPAddressField(verbose_name='IP address', + unpack_ipv4=True) created = models.DateTimeField(editable=False) iso = models.ForeignKey(Iso) -- cgit v1.2.3-54-g00ecf From 5df83a38282d8ddbc653859914eb49cad1c30494 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 4 Sep 2012 08:53:39 -0500 Subject: Add a 'format_http_headers' method This takes a HttpRequest object and grabs the HTTP headers out of it and pretty-prints them in a familiar format. This will come in handy if we want to log these when creating package FlagRequests, releng Tests, etc. in addition to already logging the IP address of the user posting the request. Signed-off-by: Dan McGee --- main/utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/main/utils.py b/main/utils.py index 0b6849a4..d12e5e1a 100644 --- a/main/utils.py +++ b/main/utils.py @@ -53,6 +53,16 @@ def clear_cache_function(func, args, kwargs): key = cache_function_key(func, args, kwargs) cache.delete(key) + +def format_http_headers(request): + headers = sorted((k, v) for k, v in request.META.items() + if k.startswith('HTTP_')) + data = [] + for k, v in headers: + data.extend([k[5:].replace('_', '-').title(), ': ', v, '\n']) + return ''.join(data) + + # utility to make a pair of django choices make_choice = lambda l: [(str(m), str(m)) for m in l] -- cgit v1.2.3-54-g00ecf From e7e9b151643772f2bf9564d215ec8b90cd9b45c6 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 5 Sep 2012 08:44:33 -0500 Subject: Split devel forms out of devel/views.py Signed-off-by: Dan McGee --- devel/forms.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++ devel/views.py | 106 ++++++--------------------------------------------------- 2 files changed, 109 insertions(+), 96 deletions(-) create mode 100644 devel/forms.py diff --git a/devel/forms.py b/devel/forms.py new file mode 100644 index 00000000..861a576c --- /dev/null +++ b/devel/forms.py @@ -0,0 +1,99 @@ +import random +from string import ascii_letters, digits + +from django import forms +from django.contrib.auth.models import User, Group +from django.contrib.sites.models import Site +from django.core.mail import send_mail +from django.template import loader, Context + +from .models import UserProfile + + +class ProfileForm(forms.Form): + email = forms.EmailField(label='Private email (not shown publicly):', + help_text="Used for out-of-date notifications, etc.") + passwd1 = forms.CharField(label='New Password', required=False, + widget=forms.PasswordInput) + passwd2 = forms.CharField(label='Confirm Password', required=False, + widget=forms.PasswordInput) + + def clean(self): + if self.cleaned_data['passwd1'] != self.cleaned_data['passwd2']: + raise forms.ValidationError('Passwords do not match.') + return self.cleaned_data + + +class UserProfileForm(forms.ModelForm): + def clean_pgp_key(self): + data = self.cleaned_data['pgp_key'] + # strip 0x prefix if provided; store uppercase + if data.startswith('0x'): + data = data[2:] + return data.upper() + + class Meta: + model = UserProfile + exclude = ('allowed_repos', 'user', 'latin_name') + + +class NewUserForm(forms.ModelForm): + username = forms.CharField(max_length=30) + 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'] + if User.objects.filter(username=username).exists(): + raise forms.ValidationError( + "A user with that username already exists.") + return username + + def save(self, commit=True): + profile = super(NewUserForm, self).save(False) + pwletters = ascii_letters + digits + password = ''.join([random.choice(pwletters) for _ in xrange(8)]) + user = User.objects.create_user(username=self.cleaned_data['username'], + 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 + if commit: + profile.save() + self.save_m2m() + + 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", + template.render(ctx), + 'Arch Website Notification ', + [user.email], + fail_silently=False) + +# vim: set ts=4 sw=4 et: diff --git a/devel/views.py b/devel/views.py index ad1f4deb..5406974e 100644 --- a/devel/views.py +++ b/devel/views.py @@ -1,31 +1,25 @@ from datetime import timedelta import operator import pytz -import random -from string import ascii_letters, digits import time -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.admin.models import LogEntry, ADDITION -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType -from django.contrib.sites.models import Site -from django.core.mail import send_mail from django.db import transaction from django.db.models import F, Count, Max from django.http import Http404 from django.shortcuts import get_object_or_404, render -from django.template import loader, Context from django.template.defaultfilters import filesizeformat from django.views.decorators.cache import never_cache from django.utils.encoding import force_unicode from django.utils.http import http_date from django.utils.timezone import now -from .models import UserProfile +from .forms import ProfileForm, UserProfileForm, NewUserForm from main.models import Package, PackageFile, TodolistPkg from main.models import Arch, Repo from news.models import News @@ -89,6 +83,7 @@ def index(request): return render(request, 'devel/index.html', page_dict) + @login_required def clock(request): devs = User.objects.filter(is_active=True).order_by( @@ -141,30 +136,6 @@ def clock(request): response['Expires'] = http_date(expire_time) return response -class ProfileForm(forms.Form): - email = forms.EmailField(label='Private email (not shown publicly):', - help_text="Used for out-of-date notifications, etc.") - passwd1 = forms.CharField(label='New Password', required=False, - widget=forms.PasswordInput) - passwd2 = forms.CharField(label='Confirm Password', required=False, - widget=forms.PasswordInput) - - def clean(self): - if self.cleaned_data['passwd1'] != self.cleaned_data['passwd2']: - raise forms.ValidationError('Passwords do not match.') - return self.cleaned_data - -class UserProfileForm(forms.ModelForm): - def clean_pgp_key(self): - data = self.cleaned_data['pgp_key'] - # strip 0x prefix if provided; store uppercase - if data.startswith('0x'): - data = data[2:] - return data.upper() - - class Meta: - model = UserProfile - exclude = ('allowed_repos', 'user', 'latin_name') @login_required @never_cache @@ -177,8 +148,9 @@ def change_profile(request): request.user.email = form.cleaned_data['email'] if form.cleaned_data['passwd1']: request.user.set_password(form.cleaned_data['passwd1']) - request.user.save() - profile_form.save() + with transaction.commit_on_success(): + request.user.save() + profile_form.save() return HttpResponseRedirect('/devel/') else: form = ProfileForm(initial={'email': request.user.email}) @@ -186,6 +158,7 @@ def change_profile(request): return render(request, 'devel/profile.html', {'form': form, 'profile_form': profile_form}) + @login_required def report(request, report_name, username=None): title = 'Developer Report' @@ -309,65 +282,6 @@ def report(request, report_name, username=None): return render(request, 'devel/packages.html', context) -class NewUserForm(forms.ModelForm): - username = forms.CharField(max_length=30) - 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'] - if User.objects.filter(username=username).exists(): - raise forms.ValidationError( - "A user with that username already exists.") - return username - - def save(self, commit=True): - profile = super(NewUserForm, self).save(False) - pwletters = ascii_letters + digits - password = ''.join([random.choice(pwletters) for _ in xrange(8)]) - user = User.objects.create_user(username=self.cleaned_data['username'], - 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 - if commit: - profile.save() - self.save_m2m() - - 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", - template.render(ctx), - 'Arch Website Notification ', - [user.email], - fail_silently=False) - def log_addition(request, obj): """Cribbed from ModelAdmin.log_addition.""" LogEntry.objects.log_action( @@ -379,17 +293,16 @@ def log_addition(request, obj): 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(): - @transaction.commit_on_success - def inner_save(): + with transaction.commit_on_success(): form.save() log_addition(request, form.instance.user) - inner_save() return HttpResponseRedirect('/admin/auth/user/%d/' % \ form.instance.user.id) else: @@ -406,6 +319,7 @@ def inner_save(): } return render(request, 'general_form.html', context) + @user_passes_test(lambda u: u.is_superuser) def admin_log(request, username=None): user = None -- cgit v1.2.3-54-g00ecf From 913eb8c53e35fbba88c62829dde4deb1426df191 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 5 Sep 2012 09:25:32 -0500 Subject: Bump django-countries requirement Signed-off-by: Dan McGee --- requirements.txt | 2 +- requirements_prod.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c5a37e3d..9dfaff18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Django==1.4.1 Markdown==2.2.0 South==0.7.6 -django-countries==1.2 +django-countries==1.3 pgpdump==1.3 pytz>=2012d diff --git a/requirements_prod.txt b/requirements_prod.txt index a31bf311..2dff0aae 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -1,7 +1,7 @@ Django==1.4.1 Markdown==2.2.0 South==0.7.6 -django-countries==1.2 +django-countries==1.3 pgpdump==1.3 psycopg2==2.4.5 pyinotify==0.9.3 -- cgit v1.2.3-54-g00ecf From acf252f7f3c0af95b3e90f07d0e4d878e4d0d776 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 5 Sep 2012 09:29:38 -0500 Subject: Add some HTML5-ization in JS of various input attributes On the login page, give focus to the username box when the page loads as well as turning autocorrection and auto-capitalization off on the username box. For the developer profile page, we can add some minor validation and typing of certain form fields that allow things like iPhone and Android to customize the presented keyboard to the user, as well as allowing browsers to do some client-side validation. Signed-off-by: Dan McGee --- sitestatic/archweb.js | 10 ++++++++++ templates/devel/profile.html | 14 ++++++++++++++ templates/registration/login.html | 11 +++++++++++ 3 files changed, 35 insertions(+) diff --git a/sitestatic/archweb.js b/sitestatic/archweb.js index 1ddf4ebf..c274a675 100644 --- a/sitestatic/archweb.js +++ b/sitestatic/archweb.js @@ -425,3 +425,13 @@ function format_filesize(size, decimals) { return size.toFixed(decimals) + ' ' + labels[label]; } + +/* HTML5 input type and attribute enhancements */ +function modify_attributes(to_change) { + /* jQuery doesn't let us change the 'type' attribute directly due to IE + woes, so instead we can clone and replace, setting the type. */ + $.each(to_change, function(id, attrs) { + var obj = $(id); + obj.replaceWith(obj.clone().attr(attrs)); + }); +} diff --git a/templates/devel/profile.html b/templates/devel/profile.html index b6580ab8..9806552a 100644 --- a/templates/devel/profile.html +++ b/templates/devel/profile.html @@ -1,4 +1,6 @@ {% extends "base.html" %} +{% load static from staticfiles %} + {% block title %}Arch Linux - Edit Profile{% endblock %} {% block content %} @@ -22,4 +24,16 @@

    Developer Profile

    + +{% load cdn %}{% jquery %} + + {% endblock %} diff --git a/templates/registration/login.html b/templates/registration/login.html index ad1ac1ea..edbb77ea 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -1,4 +1,6 @@ {% extends "base.html" %} +{% load static from staticfiles %} + {% block title %}Arch Linux - Developer Login{% endblock %} {% block content %} @@ -19,4 +21,13 @@

    Developer Login

    + +{% load cdn %}{% jquery %} + + {% endblock %} -- cgit v1.2.3-54-g00ecf From 277d71709ce18bfea64e05a6c190a5b6d555d76f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 5 Sep 2012 09:32:24 -0500 Subject: Upgrade jQuery to 1.8.1 Signed-off-by: Dan McGee --- main/templatetags/cdn.py | 2 +- sitestatic/jquery-1.7.2.min.js | 4 ---- sitestatic/jquery-1.8.1.min.js | 2 ++ 3 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 sitestatic/jquery-1.7.2.min.js create mode 100644 sitestatic/jquery-1.8.1.min.js diff --git a/main/templatetags/cdn.py b/main/templatetags/cdn.py index fb78039c..a3ed465c 100644 --- a/main/templatetags/cdn.py +++ b/main/templatetags/cdn.py @@ -7,7 +7,7 @@ @register.simple_tag def jquery(): - version = '1.7.2' + version = '1.8.1' oncdn = getattr(settings, 'CDN_ENABLED', True) if oncdn: link = 'https://ajax.googleapis.com/ajax/libs/jquery/' \ diff --git a/sitestatic/jquery-1.7.2.min.js b/sitestatic/jquery-1.7.2.min.js deleted file mode 100644 index 16ad06c5..00000000 --- a/sitestatic/jquery-1.7.2.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jQuery v1.7.2 jquery.com | jquery.org/license */ -(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"":"")+""),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;e=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
    {% include "packages/details_relatedto.html" %}
    Replaces:{% include "packages/details_relatedto.html" %}
    Conflicts: {% include "packages/details_relatedto.html" %}
    Replaces:{% include "packages/details_relatedto.html" %}Reverse Conflicts:{% for conflict in rev_conflicts %} + {% pkg_details_link conflict %}{% if not forloop.last %}, {% endif %}{% endfor %}
    a",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="
    "+""+"
    ",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="
    t
    ",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="
    ",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function( -a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&j.push({elem:this,matches:d.slice(e)});for(k=0;k0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

    ";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
    ";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*",""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
    ","
    "]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f -.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(;d1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]===""&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
    ").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/sitestatic/jquery-1.8.1.min.js b/sitestatic/jquery-1.8.1.min.js new file mode 100644 index 00000000..e7f2a292 --- /dev/null +++ b/sitestatic/jquery-1.8.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v@1.8.1 jquery.com | jquery.org/license */ +(function(a,b){function G(a){var b=F[a]={};return p.each(a.split(s),function(a,c){b[c]=!0}),b}function J(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(I,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:+d+""===d?+d:H.test(d)?p.parseJSON(d):d}catch(f){}p.data(a,c,d)}else d=b}return d}function K(a){var b;for(b in a){if(b==="data"&&p.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function ba(){return!1}function bb(){return!0}function bh(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function bi(a,b){do a=a[b];while(a&&a.nodeType!==1);return a}function bj(a,b,c){b=b||0;if(p.isFunction(b))return p.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return p.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=p.grep(a,function(a){return a.nodeType===1});if(be.test(b))return p.filter(b,d,!c);b=p.filter(b,d)}return p.grep(a,function(a,d){return p.inArray(a,b)>=0===c})}function bk(a){var b=bl.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function bC(a,b){return a.getElementsByTagName(b)[0]||a.appendChild(a.ownerDocument.createElement(b))}function bD(a,b){if(b.nodeType!==1||!p.hasData(a))return;var c,d,e,f=p._data(a),g=p._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;d").appendTo(e.body),c=b.css("display");b.remove();if(c==="none"||c===""){bI=e.body.appendChild(bI||p.extend(e.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!bJ||!bI.createElement)bJ=(bI.contentWindow||bI.contentDocument).document,bJ.write(""),bJ.close();b=bJ.body.appendChild(bJ.createElement(a)),c=bH(b,"display"),e.body.removeChild(bI)}return bS[a]=c,c}function ci(a,b,c,d){var e;if(p.isArray(b))p.each(b,function(b,e){c||ce.test(a)?d(a,e):ci(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&p.type(b)==="object")for(e in b)ci(a+"["+e+"]",b[e],c,d);else d(a,b)}function cz(a){return function(b,c){typeof b!="string"&&(c=b,b="*");var d,e,f,g=b.toLowerCase().split(s),h=0,i=g.length;if(p.isFunction(c))for(;h)[^>]*$|#([\w\-]*)$)/,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,y=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,z=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,A=/^-ms-/,B=/-([\da-z])/gi,C=function(a,b){return(b+"").toUpperCase()},D=function(){e.addEventListener?(e.removeEventListener("DOMContentLoaded",D,!1),p.ready()):e.readyState==="complete"&&(e.detachEvent("onreadystatechange",D),p.ready())},E={};p.fn=p.prototype={constructor:p,init:function(a,c,d){var f,g,h,i;if(!a)return this;if(a.nodeType)return this.context=this[0]=a,this.length=1,this;if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?f=[null,a,null]:f=u.exec(a);if(f&&(f[1]||!c)){if(f[1])return c=c instanceof p?c[0]:c,i=c&&c.nodeType?c.ownerDocument||c:e,a=p.parseHTML(f[1],i,!0),v.test(f[1])&&p.isPlainObject(c)&&this.attr.call(a,c,!0),p.merge(this,a);g=e.getElementById(f[2]);if(g&&g.parentNode){if(g.id!==f[2])return d.find(a);this.length=1,this[0]=g}return this.context=e,this.selector=a,this}return!c||c.jquery?(c||d).find(a):this.constructor(c).find(a)}return p.isFunction(a)?d.ready(a):(a.selector!==b&&(this.selector=a.selector,this.context=a.context),p.makeArray(a,this))},selector:"",jquery:"1.8.1",length:0,size:function(){return this.length},toArray:function(){return k.call(this)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=p.merge(this.constructor(),a);return d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")"),d},each:function(a,b){return p.each(this,a,b)},ready:function(a){return p.ready.promise().done(a),this},eq:function(a){return a=+a,a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(k.apply(this,arguments),"slice",k.call(arguments).join(","))},map:function(a){return this.pushStack(p.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:j,sort:[].sort,splice:[].splice},p.fn.init.prototype=p.fn,p.extend=p.fn.extend=function(){var a,c,d,e,f,g,h=arguments[0]||{},i=1,j=arguments.length,k=!1;typeof h=="boolean"&&(k=h,h=arguments[1]||{},i=2),typeof h!="object"&&!p.isFunction(h)&&(h={}),j===i&&(h=this,--i);for(;i0)return;d.resolveWith(e,[p]),p.fn.trigger&&p(e).trigger("ready").off("ready")},isFunction:function(a){return p.type(a)==="function"},isArray:Array.isArray||function(a){return p.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):E[m.call(a)]||"object"},isPlainObject:function(a){if(!a||p.type(a)!=="object"||a.nodeType||p.isWindow(a))return!1;try{if(a.constructor&&!n.call(a,"constructor")&&!n.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||n.call(a,d)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},error:function(a){throw new Error(a)},parseHTML:function(a,b,c){var d;return!a||typeof a!="string"?null:(typeof b=="boolean"&&(c=b,b=0),b=b||e,(d=v.exec(a))?[b.createElement(d[1])]:(d=p.buildFragment([a],b,c?null:[]),p.merge([],(d.cacheable?p.clone(d.fragment):d.fragment).childNodes)))},parseJSON:function(b){if(!b||typeof b!="string")return null;b=p.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(w.test(b.replace(y,"@").replace(z,"]").replace(x,"")))return(new Function("return "+b))();p.error("Invalid JSON: "+b)},parseXML:function(c){var d,e;if(!c||typeof c!="string")return null;try{a.DOMParser?(e=new DOMParser,d=e.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(f){d=b}return(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&p.error("Invalid XML: "+c),d},noop:function(){},globalEval:function(b){b&&r.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(A,"ms-").replace(B,C)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var e,f=0,g=a.length,h=g===b||p.isFunction(a);if(d){if(h){for(e in a)if(c.apply(a[e],d)===!1)break}else for(;f0&&a[0]&&a[i-1]||i===0||p.isArray(a));if(j)for(;h-1)i.splice(c,1),e&&(c<=g&&g--,c<=h&&h--)}),this},has:function(a){return p.inArray(a,i)>-1},empty:function(){return i=[],this},disable:function(){return i=j=c=b,this},disabled:function(){return!i},lock:function(){return j=b,c||l.disable(),this},locked:function(){return!j},fireWith:function(a,b){return b=b||[],b=[a,b.slice?b.slice():b],i&&(!d||j)&&(e?j.push(b):k(b)),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!d}};return l},p.extend({Deferred:function(a){var b=[["resolve","done",p.Callbacks("once memory"),"resolved"],["reject","fail",p.Callbacks("once memory"),"rejected"],["notify","progress",p.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return p.Deferred(function(c){p.each(b,function(b,d){var f=d[0],g=a[b];e[d[1]](p.isFunction(g)?function(){var a=g.apply(this,arguments);a&&p.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f+"With"](this===e?c:this,[a])}:c[f])}),a=null}).promise()},promise:function(a){return typeof a=="object"?p.extend(a,d):d}},e={};return d.pipe=d.then,p.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[a^1][2].disable,b[2][2].lock),e[f[0]]=g.fire,e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=k.call(arguments),d=c.length,e=d!==1||a&&p.isFunction(a.promise)?d:0,f=e===1?a:p.Deferred(),g=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?k.call(arguments):d,c===h?f.notifyWith(b,c):--e||f.resolveWith(b,c)}},h,i,j;if(d>1){h=new Array(d),i=new Array(d),j=new Array(d);for(;b
    a",c=n.getElementsByTagName("*"),d=n.getElementsByTagName("a")[0],d.style.cssText="top:1px;float:left;opacity:.5";if(!c||!c.length||!d)return{};f=e.createElement("select"),g=f.appendChild(e.createElement("option")),h=n.getElementsByTagName("input")[0],b={leadingWhitespace:n.firstChild.nodeType===3,tbody:!n.getElementsByTagName("tbody").length,htmlSerialize:!!n.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.5/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:h.value==="on",optSelected:g.selected,getSetAttribute:n.className!=="t",enctype:!!e.createElement("form").enctype,html5Clone:e.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",boxModel:e.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},h.checked=!0,b.noCloneChecked=h.cloneNode(!0).checked,f.disabled=!0,b.optDisabled=!g.disabled;try{delete n.test}catch(o){b.deleteExpando=!1}!n.addEventListener&&n.attachEvent&&n.fireEvent&&(n.attachEvent("onclick",m=function(){b.noCloneEvent=!1}),n.cloneNode(!0).fireEvent("onclick"),n.detachEvent("onclick",m)),h=e.createElement("input"),h.value="t",h.setAttribute("type","radio"),b.radioValue=h.value==="t",h.setAttribute("checked","checked"),h.setAttribute("name","t"),n.appendChild(h),i=e.createDocumentFragment(),i.appendChild(n.lastChild),b.checkClone=i.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=h.checked,i.removeChild(h),i.appendChild(n);if(n.attachEvent)for(k in{submit:!0,change:!0,focusin:!0})j="on"+k,l=j in n,l||(n.setAttribute(j,"return;"),l=typeof n[j]=="function"),b[k+"Bubbles"]=l;return p(function(){var c,d,f,g,h="padding:0;margin:0;border:0;display:block;overflow:hidden;",i=e.getElementsByTagName("body")[0];if(!i)return;c=e.createElement("div"),c.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",i.insertBefore(c,i.firstChild),d=e.createElement("div"),c.appendChild(d),d.innerHTML="
    t
    ",f=d.getElementsByTagName("td"),f[0].style.cssText="padding:0;margin:0;border:0;display:none",l=f[0].offsetHeight===0,f[0].style.display="",f[1].style.display="none",b.reliableHiddenOffsets=l&&f[0].offsetHeight===0,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",b.boxSizing=d.offsetWidth===4,b.doesNotIncludeMarginInBodyOffset=i.offsetTop!==1,a.getComputedStyle&&(b.pixelPosition=(a.getComputedStyle(d,null)||{}).top!=="1%",b.boxSizingReliable=(a.getComputedStyle(d,null)||{width:"4px"}).width==="4px",g=e.createElement("div"),g.style.cssText=d.style.cssText=h,g.style.marginRight=g.style.width="0",d.style.width="1px",d.appendChild(g),b.reliableMarginRight=!parseFloat((a.getComputedStyle(g,null)||{}).marginRight)),typeof d.style.zoom!="undefined"&&(d.innerHTML="",d.style.cssText=h+"width:1px;padding:1px;display:inline;zoom:1",b.inlineBlockNeedsLayout=d.offsetWidth===3,d.style.display="block",d.style.overflow="visible",d.innerHTML="
    ",d.firstChild.style.width="5px",b.shrinkWrapBlocks=d.offsetWidth!==3,c.style.zoom=1),i.removeChild(c),c=d=f=g=null}),i.removeChild(n),c=d=f=g=h=i=n=null,b}();var H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,I=/([A-Z])/g;p.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(p.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){return a=a.nodeType?p.cache[a[p.expando]]:a[p.expando],!!a&&!K(a)},data:function(a,c,d,e){if(!p.acceptData(a))return;var f,g,h=p.expando,i=typeof c=="string",j=a.nodeType,k=j?p.cache:a,l=j?a[h]:a[h]&&h;if((!l||!k[l]||!e&&!k[l].data)&&i&&d===b)return;l||(j?a[h]=l=p.deletedIds.pop()||++p.uuid:l=h),k[l]||(k[l]={},j||(k[l].toJSON=p.noop));if(typeof c=="object"||typeof c=="function")e?k[l]=p.extend(k[l],c):k[l].data=p.extend(k[l].data,c);return f=k[l],e||(f.data||(f.data={}),f=f.data),d!==b&&(f[p.camelCase(c)]=d),i?(g=f[c],g==null&&(g=f[p.camelCase(c)])):g=f,g},removeData:function(a,b,c){if(!p.acceptData(a))return;var d,e,f,g=a.nodeType,h=g?p.cache:a,i=g?a[p.expando]:p.expando;if(!h[i])return;if(b){d=c?h[i]:h[i].data;if(d){p.isArray(b)||(b in d?b=[b]:(b=p.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,f=b.length;e1,null,!1))},removeData:function(a){return this.each(function(){p.removeData(this,a)})}}),p.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=p._data(a,b),c&&(!d||p.isArray(c)?d=p._data(a,b,p.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=p.queue(a,b),d=c.length,e=c.shift(),f=p._queueHooks(a,b),g=function(){p.dequeue(a,b)};e==="inprogress"&&(e=c.shift(),d--),e&&(b==="fx"&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return p._data(a,c)||p._data(a,c,{empty:p.Callbacks("once memory").add(function(){p.removeData(a,b+"queue",!0),p.removeData(a,c,!0)})})}}),p.fn.extend({queue:function(a,c){var d=2;return typeof a!="string"&&(c=a,a="fx",d--),arguments.length1)},removeAttr:function(a){return this.each(function(){p.removeAttr(this,a)})},prop:function(a,b){return p.access(this,p.prop,a,b,arguments.length>1)},removeProp:function(a){return a=p.propFix[a]||a,this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,f,g,h;if(p.isFunction(a))return this.each(function(b){p(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(s);for(c=0,d=this.length;c-1)d=d.replace(" "+c[f]+" "," ");e.className=a?p.trim(d):""}}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";return p.isFunction(a)?this.each(function(c){p(this).toggleClass(a.call(this,c,this.className,b),b)}):this.each(function(){if(c==="string"){var e,f=0,g=p(this),h=b,i=a.split(s);while(e=i[f++])h=d?h:!g.hasClass(e),g[h?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&p._data(this,"__className__",this.className),this.className=this.className||a===!1?"":p._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c-1)return!0;return!1},val:function(a){var c,d,e,f=this[0];if(!arguments.length){if(f)return c=p.valHooks[f.type]||p.valHooks[f.nodeName.toLowerCase()],c&&"get"in c&&(d=c.get(f,"value"))!==b?d:(d=f.value,typeof d=="string"?d.replace(P,""):d==null?"":d);return}return e=p.isFunction(a),this.each(function(d){var f,g=p(this);if(this.nodeType!==1)return;e?f=a.call(this,d,g.val()):f=a,f==null?f="":typeof f=="number"?f+="":p.isArray(f)&&(f=p.map(f,function(a){return a==null?"":a+""})),c=p.valHooks[this.type]||p.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,f,"value")===b)this.value=f})}}),p.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,f=a.selectedIndex,g=[],h=a.options,i=a.type==="select-one";if(f<0)return null;c=i?f:0,d=i?f+1:h.length;for(;c=0}),c.length||(a.selectedIndex=-1),c}}},attrFn:{},attr:function(a,c,d,e){var f,g,h,i=a.nodeType;if(!a||i===3||i===8||i===2)return;if(e&&p.isFunction(p.fn[c]))return p(a)[c](d);if(typeof a.getAttribute=="undefined")return p.prop(a,c,d);h=i!==1||!p.isXMLDoc(a),h&&(c=c.toLowerCase(),g=p.attrHooks[c]||(T.test(c)?M:L));if(d!==b){if(d===null){p.removeAttr(a,c);return}return g&&"set"in g&&h&&(f=g.set(a,d,c))!==b?f:(a.setAttribute(c,""+d),d)}return g&&"get"in g&&h&&(f=g.get(a,c))!==null?f:(f=a.getAttribute(c),f===null?b:f)},removeAttr:function(a,b){var c,d,e,f,g=0;if(b&&a.nodeType===1){d=b.split(s);for(;g=0}})});var V=/^(?:textarea|input|select)$/i,W=/^([^\.]*|)(?:\.(.+)|)$/,X=/(?:^|\s)hover(\.\S+|)\b/,Y=/^key/,Z=/^(?:mouse|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=function(a){return p.event.special.hover?a:a.replace(X,"mouseenter$1 mouseleave$1")};p.event={add:function(a,c,d,e,f){var g,h,i,j,k,l,m,n,o,q,r;if(a.nodeType===3||a.nodeType===8||!c||!d||!(g=p._data(a)))return;d.handler&&(o=d,d=o.handler,f=o.selector),d.guid||(d.guid=p.guid++),i=g.events,i||(g.events=i={}),h=g.handle,h||(g.handle=h=function(a){return typeof p!="undefined"&&(!a||p.event.triggered!==a.type)?p.event.dispatch.apply(h.elem,arguments):b},h.elem=a),c=p.trim(_(c)).split(" ");for(j=0;j=0&&(s=s.slice(0,-1),i=!0),s.indexOf(".")>=0&&(t=s.split("."),s=t.shift(),t.sort());if((!f||p.event.customEvent[s])&&!p.event.global[s])return;c=typeof c=="object"?c[p.expando]?c:new p.Event(s,c):new p.Event(s),c.type=s,c.isTrigger=!0,c.exclusive=i,c.namespace=t.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+t.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,m=s.indexOf(":")<0?"on"+s:"";if(!f){h=p.cache;for(j in h)h[j].events&&h[j].events[s]&&p.event.trigger(c,d,h[j].handle.elem,!0);return}c.result=b,c.target||(c.target=f),d=d!=null?p.makeArray(d):[],d.unshift(c),n=p.event.special[s]||{};if(n.trigger&&n.trigger.apply(f,d)===!1)return;q=[[f,n.bindType||s]];if(!g&&!n.noBubble&&!p.isWindow(f)){r=n.delegateType||s,k=$.test(r+s)?f:f.parentNode;for(l=f;k;k=k.parentNode)q.push([k,r]),l=k;l===(f.ownerDocument||e)&&q.push([l.defaultView||l.parentWindow||a,r])}for(j=0;j=0),h[l]&&j.push(k);j.length&&t.push({elem:f,matches:j})}n.length>o&&t.push({elem:this,matches:n.slice(o)});for(d=0;d0?this.on(b,null,a,c):this.trigger(b)},Y.test(b)&&(p.event.fixHooks[b]=p.event.keyHooks),Z.test(b)&&(p.event.fixHooks[b]=p.event.mouseHooks)}),function(a,b){function $(a,b,c,d){c=c||[],b=b||q;var e,f,g,j,k=b.nodeType;if(k!==1&&k!==9)return[];if(!a||typeof a!="string")return c;g=h(b);if(!g&&!d)if(e=L.exec(a))if(j=e[1]){if(k===9){f=b.getElementById(j);if(!f||!f.parentNode)return c;if(f.id===j)return c.push(f),c}else if(b.ownerDocument&&(f=b.ownerDocument.getElementById(j))&&i(b,f)&&f.id===j)return c.push(f),c}else{if(e[2])return u.apply(c,t.call(b.getElementsByTagName(a),0)),c;if((j=e[3])&&X&&b.getElementsByClassName)return u.apply(c,t.call(b.getElementsByClassName(j),0)),c}return bk(a,b,c,d,g)}function _(a){return function(b){var c=b.nodeName.toLowerCase();return c==="input"&&b.type===a}}function ba(a){return function(b){var c=b.nodeName.toLowerCase();return(c==="input"||c==="button")&&b.type===a}}function bb(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}function bc(a,b,c,d){var e,g,h,i,j,k,l,m,n,p,r=!c&&b!==q,s=(r?"":"")+a.replace(H,"$1"),u=y[o][s];if(u)return d?0:t.call(u,0);j=a,k=[],m=0,n=f.preFilter,p=f.filter;while(j){if(!e||(g=I.exec(j)))g&&(j=j.slice(g[0].length),h.selector=l),k.push(h=[]),l="",r&&(j=" "+j);e=!1;if(g=J.exec(j))l+=g[0],j=j.slice(g[0].length),e=h.push({part:g.pop().replace(H," "),string:g[0],captures:g});for(i in p)(g=S[i].exec(j))&&(!n[i]||(g=n[i](g,b,c)))&&(l+=g[0],j=j.slice(g[0].length),e=h.push({part:i,string:g.shift(),captures:g}));if(!e)break}return l&&(h.selector=l),d?j.length:j?$.error(a):t.call(y(s,k),0)}function bd(a,b,e,f){var g=b.dir,h=s++;return a||(a=function(a){return a===e}),b.first?function(b){while(b=b[g])if(b.nodeType===1)return a(b)&&b}:f?function(b){while(b=b[g])if(b.nodeType===1&&a(b))return b}:function(b){var e,f=h+"."+c,i=f+"."+d;while(b=b[g])if(b.nodeType===1){if((e=b[o])===i)return b.sizset;if(typeof e=="string"&&e.indexOf(f)===0){if(b.sizset)return b}else{b[o]=i;if(a(b))return b.sizset=!0,b;b.sizset=!1}}}}function be(a,b){return a?function(c){var d=b(c);return d&&a(d===!0?c:d)}:b}function bf(a,b,c){var d,e,g=0;for(;d=a[g];g++)f.relative[d.part]?e=bd(e,f.relative[d.part],b,c):e=be(e,f.filter[d.part].apply(null,d.captures.concat(b,c)));return e}function bg(a){return function(b){var c,d=0;for(;c=a[d];d++)if(c(b))return!0;return!1}}function bh(a,b,c,d){var e=0,f=b.length;for(;e0?i(h,c,g):[]}function bj(a,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q,r,s=0,t=a.length,v=S.POS,w=new RegExp("^"+v.source+"(?!"+A+")","i"),x=function(){var a=1,c=arguments.length-2;for(;al){g+=k.slice(l,n.index),l=p,q=[c],J.test(g)&&(m&&(q=m),m=e);if(r=O.test(g))g=g.slice(0,-5).replace(J,"$&*"),l++;n.length>1&&n[0].replace(w,x),m=bi(g,n[1],n[2],q,m,r)}g=""}}o||(g+=k),o=!1}g?J.test(g)?bh(g,m||[c],d,e):$(g,c,d,e?e.concat(m):m):u.apply(d,m)}return t===1?d:$.uniqueSort(d)}function bk(a,b,e,g,h){a=a.replace(H,"$1");var i,k,l,m,n,o,p,q,r,s,v=bc(a,b,h),w=b.nodeType;if(S.POS.test(a))return bj(v,b,e,g);if(g)i=t.call(g,0);else if(v.length===1){if((o=t.call(v[0],0)).length>2&&(p=o[0]).part==="ID"&&w===9&&!h&&f.relative[o[1].part]){b=f.find.ID(p.captures[0].replace(R,""),b,h)[0];if(!b)return e;a=a.slice(o.shift().string.length)}r=(v=N.exec(o[0].string))&&!v.index&&b.parentNode||b,q="";for(n=o.length-1;n>=0;n--){p=o[n],s=p.part,q=p.string+q;if(f.relative[s])break;if(f.order.test(s)){i=f.find[s](p.captures[0].replace(R,""),r,h);if(i==null)continue;a=a.slice(0,a.length-q.length)+q.replace(S[s],""),a||u.apply(e,t.call(i,0));break}}}if(a){k=j(a,b,h),c=k.dirruns++,i==null&&(i=f.find.TAG("*",N.test(a)&&b.parentNode||b));for(n=0;m=i[n];n++)d=k.runs++,k(m)&&e.push(m)}return e}var c,d,e,f,g,h,i,j,k,l,m=!0,n="undefined",o=("sizcache"+Math.random()).replace(".",""),q=a.document,r=q.documentElement,s=0,t=[].slice,u=[].push,v=function(a,b){return a[o]=b||!0,a},w=function(){var a={},b=[];return v(function(c,d){return b.push(c)>f.cacheLength&&delete a[b.shift()],a[c]=d},a)},x=w(),y=w(),z=w(),A="[\\x20\\t\\r\\n\\f]",B="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",C=B.replace("w","w#"),D="([*^$|!~]?=)",E="\\["+A+"*("+B+")"+A+"*(?:"+D+A+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+C+")|)|)"+A+"*\\]",F=":("+B+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+E+")|[^:]|\\\\.)*|.*))\\)|)",G=":(nth|eq|gt|lt|first|last|even|odd)(?:\\(((?:-\\d)?\\d*)\\)|)(?=[^-]|$)",H=new RegExp("^"+A+"+|((?:^|[^\\\\])(?:\\\\.)*)"+A+"+$","g"),I=new RegExp("^"+A+"*,"+A+"*"),J=new RegExp("^"+A+"*([\\x20\\t\\r\\n\\f>+~])"+A+"*"),K=new RegExp(F),L=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,M=/^:not/,N=/[\x20\t\r\n\f]*[+~]/,O=/:not\($/,P=/h\d/i,Q=/input|select|textarea|button/i,R=/\\(?!\\)/g,S={ID:new RegExp("^#("+B+")"),CLASS:new RegExp("^\\.("+B+")"),NAME:new RegExp("^\\[name=['\"]?("+B+")['\"]?\\]"),TAG:new RegExp("^("+B.replace("w","w*")+")"),ATTR:new RegExp("^"+E),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|nth|last|first)-child(?:\\("+A+"*(even|odd|(([+-]|)(\\d*)n|)"+A+"*(?:([+-]|)"+A+"*(\\d+)|))"+A+"*\\)|)","i"),POS:new RegExp(G,"ig"),needsContext:new RegExp("^"+A+"*[>+~]|"+G,"i")},T=function(a){var b=q.createElement("div");try{return a(b)}catch(c){return!1}finally{b=null}},U=T(function(a){return a.appendChild(q.createComment("")),!a.getElementsByTagName("*").length}),V=T(function(a){return a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!==n&&a.firstChild.getAttribute("href")==="#"}),W=T(function(a){a.innerHTML="";var b=typeof a.lastChild.getAttribute("multiple");return b!=="boolean"&&b!=="string"}),X=T(function(a){return a.innerHTML="",!a.getElementsByClassName||!a.getElementsByClassName("e").length?!1:(a.lastChild.className="e",a.getElementsByClassName("e").length===2)}),Y=T(function(a){a.id=o+0,a.innerHTML="
    ",r.insertBefore(a,r.firstChild);var b=q.getElementsByName&&q.getElementsByName(o).length===2+q.getElementsByName(o+0).length;return e=!q.getElementById(o),r.removeChild(a),b});try{t.call(r.childNodes,0)[0].nodeType}catch(Z){t=function(a){var b,c=[];for(;b=this[a];a++)c.push(b);return c}}$.matches=function(a,b){return $(a,null,null,b)},$.matchesSelector=function(a,b){return $(b,null,null,[a]).length>0},g=$.getText=function(a){var b,c="",d=0,e=a.nodeType;if(e){if(e===1||e===9||e===11){if(typeof a.textContent=="string")return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=g(a)}else if(e===3||e===4)return a.nodeValue}else for(;b=a[d];d++)c+=g(b);return c},h=$.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?b.nodeName!=="HTML":!1},i=$.contains=r.contains?function(a,b){var c=a.nodeType===9?a.documentElement:a,d=b&&b.parentNode;return a===d||!!(d&&d.nodeType===1&&c.contains&&c.contains(d))}:r.compareDocumentPosition?function(a,b){return b&&!!(a.compareDocumentPosition(b)&16)}:function(a,b){while(b=b.parentNode)if(b===a)return!0;return!1},$.attr=function(a,b){var c,d=h(a);return d||(b=b.toLowerCase()),f.attrHandle[b]?f.attrHandle[b](a):W||d?a.getAttribute(b):(c=a.getAttributeNode(b),c?typeof a[b]=="boolean"?a[b]?b:null:c.specified?c.value:null:null)},f=$.selectors={cacheLength:50,createPseudo:v,match:S,order:new RegExp("ID|TAG"+(Y?"|NAME":"")+(X?"|CLASS":"")),attrHandle:V?{}:{href:function(a){return a.getAttribute("href",2)},type:function(a){return a.getAttribute("type")}},find:{ID:e?function(a,b,c){if(typeof b.getElementById!==n&&!c){var d=b.getElementById(a);return d&&d.parentNode?[d]:[]}}:function(a,c,d){if(typeof c.getElementById!==n&&!d){var e=c.getElementById(a);return e?e.id===a||typeof e.getAttributeNode!==n&&e.getAttributeNode("id").value===a?[e]:b:[]}},TAG:U?function(a,b){if(typeof b.getElementsByTagName!==n)return b.getElementsByTagName(a)}:function(a,b){var c=b.getElementsByTagName(a);if(a==="*"){var d,e=[],f=0;for(;d=c[f];f++)d.nodeType===1&&e.push(d);return e}return c},NAME:function(a,b){if(typeof b.getElementsByName!==n)return b.getElementsByName(name)},CLASS:function(a,b,c){if(typeof b.getElementsByClassName!==n&&!c)return b.getElementsByClassName(a)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(R,""),a[3]=(a[4]||a[5]||"").replace(R,""),a[2]==="~="&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),a[1]==="nth"?(a[2]||$.error(a[0]),a[3]=+(a[3]?a[4]+(a[5]||1):2*(a[2]==="even"||a[2]==="odd")),a[4]=+(a[6]+a[7]||a[2]==="odd")):a[2]&&$.error(a[0]),a},PSEUDO:function(a,b,c){var d,e;if(S.CHILD.test(a[0]))return null;if(a[3])a[2]=a[3];else if(d=a[4])K.test(d)&&(e=bc(d,b,c,!0))&&(e=d.indexOf(")",d.length-e)-d.length)&&(d=d.slice(0,e),a[0]=a[0].slice(0,e)),a[2]=d;return a.slice(0,3)}},filter:{ID:e?function(a){return a=a.replace(R,""),function(b){return b.getAttribute("id")===a}}:function(a){return a=a.replace(R,""),function(b){var c=typeof b.getAttributeNode!==n&&b.getAttributeNode("id");return c&&c.value===a}},TAG:function(a){return a==="*"?function(){return!0}:(a=a.replace(R,"").toLowerCase(),function(b){return b.nodeName&&b.nodeName.toLowerCase()===a})},CLASS:function(a){var b=x[o][a];return b||(b=x(a,new RegExp("(^|"+A+")"+a+"("+A+"|$)"))),function(a){return b.test(a.className||typeof a.getAttribute!==n&&a.getAttribute("class")||"")}},ATTR:function(a,b,c){return b?function(d){var e=$.attr(d,a),f=e+"";if(e==null)return b==="!=";switch(b){case"=":return f===c;case"!=":return f!==c;case"^=":return c&&f.indexOf(c)===0;case"*=":return c&&f.indexOf(c)>-1;case"$=":return c&&f.substr(f.length-c.length)===c;case"~=":return(" "+f+" ").indexOf(c)>-1;case"|=":return f===c||f.substr(0,c.length+1)===c+"-"}}:function(b){return $.attr(b,a)!=null}},CHILD:function(a,b,c,d){if(a==="nth"){var e=s++;return function(a){var b,f,g=0,h=a;if(c===1&&d===0)return!0;b=a.parentNode;if(b&&(b[o]!==e||!a.sizset)){for(h=b.firstChild;h;h=h.nextSibling)if(h.nodeType===1){h.sizset=++g;if(h===a)break}b[o]=e}return f=a.sizset-d,c===0?f===0:f%c===0&&f/c>=0}}return function(b){var c=b;switch(a){case"only":case"first":while(c=c.previousSibling)if(c.nodeType===1)return!1;if(a==="first")return!0;c=b;case"last":while(c=c.nextSibling)if(c.nodeType===1)return!1;return!0}}},PSEUDO:function(a,b,c,d){var e,g=f.pseudos[a]||f.pseudos[a.toLowerCase()];return g||$.error("unsupported pseudo: "+a),g[o]?g(b,c,d):g.length>1?(e=[a,a,"",b],function(a){return g(a,0,e)}):g}},pseudos:{not:v(function(a,b,c){var d=j(a.replace(H,"$1"),b,c);return function(a){return!d(a)}}),enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&!!a.checked||b==="option"&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},parent:function(a){return!f.pseudos.empty(a)},empty:function(a){var b;a=a.firstChild;while(a){if(a.nodeName>"@"||(b=a.nodeType)===3||b===4)return!1;a=a.nextSibling}return!0},contains:v(function(a){return function(b){return(b.textContent||b.innerText||g(b)).indexOf(a)>-1}}),has:v(function(a){return function(b){return $(a,b).length>0}}),header:function(a){return P.test(a.nodeName)},text:function(a){var b,c;return a.nodeName.toLowerCase()==="input"&&(b=a.type)==="text"&&((c=a.getAttribute("type"))==null||c.toLowerCase()===b)},radio:_("radio"),checkbox:_("checkbox"),file:_("file"),password:_("password"),image:_("image"),submit:ba("submit"),reset:ba("reset"),button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&a.type==="button"||b==="button"},input:function(a){return Q.test(a.nodeName)},focus:function(a){var b=a.ownerDocument;return a===b.activeElement&&(!b.hasFocus||b.hasFocus())&&(!!a.type||!!a.href)},active:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b,c){return c?a.slice(1):[a[0]]},last:function(a,b,c){var d=a.pop();return c?a:[d]},even:function(a,b,c){var d=[],e=c?1:0,f=a.length;for(;e",a.querySelectorAll("[selected]").length||e.push("\\["+A+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),a.querySelectorAll(":checked").length||e.push(":checked")}),T(function(a){a.innerHTML="

    ",a.querySelectorAll("[test^='']").length&&e.push("[*^$]="+A+"*(?:\"\"|'')"),a.innerHTML="",a.querySelectorAll(":enabled").length||e.push(":enabled",":disabled")}),e=e.length&&new RegExp(e.join("|")),bk=function(a,d,f,g,h){if(!g&&!h&&(!e||!e.test(a)))if(d.nodeType===9)try{return u.apply(f,t.call(d.querySelectorAll(a),0)),f}catch(i){}else if(d.nodeType===1&&d.nodeName.toLowerCase()!=="object"){var j,k,l,m=d.getAttribute("id"),n=m||o,p=N.test(a)&&d.parentNode||d;m?n=n.replace(c,"\\$&"):d.setAttribute("id",n),j=bc(a,d,h),n="[id='"+n+"']";for(k=0,l=j.length;k0})}(),f.setFilters.nth=f.setFilters.eq,f.filters=f.pseudos,$.attr=p.attr,p.find=$,p.expr=$.selectors,p.expr[":"]=p.expr.pseudos,p.unique=$.uniqueSort,p.text=$.getText,p.isXMLDoc=$.isXML,p.contains=$.contains}(a);var bc=/Until$/,bd=/^(?:parents|prev(?:Until|All))/,be=/^.[^:#\[\.,]*$/,bf=p.expr.match.needsContext,bg={children:!0,contents:!0,next:!0,prev:!0};p.fn.extend({find:function(a){var b,c,d,e,f,g,h=this;if(typeof a!="string")return p(a).filter(function(){for(b=0,c=h.length;b0)for(e=d;e=0:p.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c,d=0,e=this.length,f=[],g=bf.test(a)||typeof a!="string"?p(a,b||this.context):0;for(;d-1:p.find.matchesSelector(c,a)){f.push(c);break}c=c.parentNode}}return f=f.length>1?p.unique(f):f,this.pushStack(f,"closest",a)},index:function(a){return a?typeof a=="string"?p.inArray(this[0],p(a)):p.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(a,b){var c=typeof a=="string"?p(a,b):p.makeArray(a&&a.nodeType?[a]:a),d=p.merge(this.get(),c);return this.pushStack(bh(c[0])||bh(d[0])?d:p.unique(d))},addBack:function(a){return this.add(a==null?this.prevObject:this.prevObject.filter(a))}}),p.fn.andSelf=p.fn.addBack,p.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return p.dir(a,"parentNode")},parentsUntil:function(a,b,c){return p.dir(a,"parentNode",c)},next:function(a){return bi(a,"nextSibling")},prev:function(a){return bi(a,"previousSibling")},nextAll:function(a){return p.dir(a,"nextSibling")},prevAll:function(a){return p.dir(a,"previousSibling")},nextUntil:function(a,b,c){return p.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return p.dir(a,"previousSibling",c)},siblings:function(a){return p.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return p.sibling(a.firstChild)},contents:function(a){return p.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:p.merge([],a.childNodes)}},function(a,b){p.fn[a]=function(c,d){var e=p.map(this,b,c);return bc.test(a)||(d=c),d&&typeof d=="string"&&(e=p.filter(d,e)),e=this.length>1&&!bg[a]?p.unique(e):e,this.length>1&&bd.test(a)&&(e=e.reverse()),this.pushStack(e,a,k.call(arguments).join(","))}}),p.extend({filter:function(a,b,c){return c&&(a=":not("+a+")"),b.length===1?p.find.matchesSelector(b[0],a)?[b[0]]:[]:p.find.matches(a,b)},dir:function(a,c,d){var e=[],f=a[c];while(f&&f.nodeType!==9&&(d===b||f.nodeType!==1||!p(f).is(d)))f.nodeType===1&&e.push(f),f=f[c];return e},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var bl="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",bm=/ jQuery\d+="(?:null|\d+)"/g,bn=/^\s+/,bo=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bp=/<([\w:]+)/,bq=/]","i"),bv=/^(?:checkbox|radio)$/,bw=/checked\s*(?:[^=]|=\s*.checked.)/i,bx=/\/(java|ecma)script/i,by=/^\s*\s*$/g,bz={option:[1,""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]},bA=bk(e),bB=bA.appendChild(e.createElement("div"));bz.optgroup=bz.option,bz.tbody=bz.tfoot=bz.colgroup=bz.caption=bz.thead,bz.th=bz.td,p.support.htmlSerialize||(bz._default=[1,"X
    ","
    "]),p.fn.extend({text:function(a){return p.access(this,function(a){return a===b?p.text(this):this.empty().append((this[0]&&this[0].ownerDocument||e).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(p.isFunction(a))return this.each(function(b){p(this).wrapAll(a.call(this,b))});if(this[0]){var b=p(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return p.isFunction(a)?this.each(function(b){p(this).wrapInner(a.call(this,b))}):this.each(function(){var b=p(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=p.isFunction(a);return this.each(function(c){p(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){p.nodeName(this,"body")||p(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(a,this.firstChild)})},before:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(a,this),"before",this.selector)}},after:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(this,a),"after",this.selector)}},remove:function(a,b){var c,d=0;for(;(c=this[d])!=null;d++)if(!a||p.filter(a,[c]).length)!b&&c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),p.cleanData([c])),c.parentNode&&c.parentNode.removeChild(c);return this},empty:function(){var a,b=0;for(;(a=this[b])!=null;b++){a.nodeType===1&&p.cleanData(a.getElementsByTagName("*"));while(a.firstChild)a.removeChild(a.firstChild)}return this},clone:function(a,b){return a=a==null?!1:a,b=b==null?a:b,this.map(function(){return p.clone(this,a,b)})},html:function(a){return p.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(bm,""):b;if(typeof a=="string"&&!bs.test(a)&&(p.support.htmlSerialize||!bu.test(a))&&(p.support.leadingWhitespace||!bn.test(a))&&!bz[(bp.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(bo,"<$1>");try{for(;d1&&typeof j=="string"&&bw.test(j))return this.each(function(){p(this).domManip(a,c,d)});if(p.isFunction(j))return this.each(function(e){var f=p(this);a[0]=j.call(this,e,c?f.html():b),f.domManip(a,c,d)});if(this[0]){e=p.buildFragment(a,this,k),g=e.fragment,f=g.firstChild,g.childNodes.length===1&&(g=f);if(f){c=c&&p.nodeName(f,"tr");for(h=e.cacheable||l-1;i0?this.clone(!0):this).get(),p(g[e])[b](d),f=f.concat(d);return this.pushStack(f,a,g.selector)}}),p.extend({clone:function(a,b,c){var d,e,f,g;p.support.html5Clone||p.isXMLDoc(a)||!bu.test("<"+a.nodeName+">")?g=a.cloneNode(!0):(bB.innerHTML=a.outerHTML,bB.removeChild(g=bB.firstChild));if((!p.support.noCloneEvent||!p.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!p.isXMLDoc(a)){bE(a,g),d=bF(a),e=bF(g);for(f=0;d[f];++f)e[f]&&bE(d[f],e[f])}if(b){bD(a,g);if(c){d=bF(a),e=bF(g);for(f=0;d[f];++f)bD(d[f],e[f])}}return d=e=null,g},clean:function(a,b,c,d){var f,g,h,i,j,k,l,m,n,o,q,r,s=b===e&&bA,t=[];if(!b||typeof b.createDocumentFragment=="undefined")b=e;for(f=0;(h=a[f])!=null;f++){typeof h=="number"&&(h+="");if(!h)continue;if(typeof h=="string")if(!br.test(h))h=b.createTextNode(h);else{s=s||bk(b),l=b.createElement("div"),s.appendChild(l),h=h.replace(bo,"<$1>"),i=(bp.exec(h)||["",""])[1].toLowerCase(),j=bz[i]||bz._default,k=j[0],l.innerHTML=j[1]+h+j[2];while(k--)l=l.lastChild;if(!p.support.tbody){m=bq.test(h),n=i==="table"&&!m?l.firstChild&&l.firstChild.childNodes:j[1]===""&&!m?l.childNodes:[];for(g=n.length-1;g>=0;--g)p.nodeName(n[g],"tbody")&&!n[g].childNodes.length&&n[g].parentNode.removeChild(n[g])}!p.support.leadingWhitespace&&bn.test(h)&&l.insertBefore(b.createTextNode(bn.exec(h)[0]),l.firstChild),h=l.childNodes,l.parentNode.removeChild(l)}h.nodeType?t.push(h):p.merge(t,h)}l&&(h=l=s=null);if(!p.support.appendChecked)for(f=0;(h=t[f])!=null;f++)p.nodeName(h,"input")?bG(h):typeof h.getElementsByTagName!="undefined"&&p.grep(h.getElementsByTagName("input"),bG);if(c){q=function(a){if(!a.type||bx.test(a.type))return d?d.push(a.parentNode?a.parentNode.removeChild(a):a):c.appendChild(a)};for(f=0;(h=t[f])!=null;f++)if(!p.nodeName(h,"script")||!q(h))c.appendChild(h),typeof h.getElementsByTagName!="undefined"&&(r=p.grep(p.merge([],h.getElementsByTagName("script")),q),t.splice.apply(t,[f+1,0].concat(r)),f+=r.length)}return t},cleanData:function(a,b){var c,d,e,f,g=0,h=p.expando,i=p.cache,j=p.support.deleteExpando,k=p.event.special;for(;(e=a[g])!=null;g++)if(b||p.acceptData(e)){d=e[h],c=d&&i[d];if(c){if(c.events)for(f in c.events)k[f]?p.event.remove(e,f):p.removeEvent(e,f,c.handle);i[d]&&(delete i[d],j?delete e[h]:e.removeAttribute?e.removeAttribute(h):e[h]=null,p.deletedIds.push(d))}}}}),function(){var a,b;p.uaMatch=function(a){a=a.toLowerCase();var b=/(chrome)[ \/]([\w.]+)/.exec(a)||/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||a.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},a=p.uaMatch(g.userAgent),b={},a.browser&&(b[a.browser]=!0,b.version=a.version),b.chrome?b.webkit=!0:b.webkit&&(b.safari=!0),p.browser=b,p.sub=function(){function a(b,c){return new a.fn.init(b,c)}p.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function c(c,d){return d&&d instanceof p&&!(d instanceof a)&&(d=a(d)),p.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(e);return a}}();var bH,bI,bJ,bK=/alpha\([^)]*\)/i,bL=/opacity=([^)]*)/,bM=/^(top|right|bottom|left)$/,bN=/^(none|table(?!-c[ea]).+)/,bO=/^margin/,bP=new RegExp("^("+q+")(.*)$","i"),bQ=new RegExp("^("+q+")(?!px)[a-z%]+$","i"),bR=new RegExp("^([-+])=("+q+")","i"),bS={},bT={position:"absolute",visibility:"hidden",display:"block"},bU={letterSpacing:0,fontWeight:400},bV=["Top","Right","Bottom","Left"],bW=["Webkit","O","Moz","ms"],bX=p.fn.toggle;p.fn.extend({css:function(a,c){return p.access(this,function(a,c,d){return d!==b?p.style(a,c,d):p.css(a,c)},a,c,arguments.length>1)},show:function(){return b$(this,!0)},hide:function(){return b$(this)},toggle:function(a,b){var c=typeof a=="boolean";return p.isFunction(a)&&p.isFunction(b)?bX.apply(this,arguments):this.each(function(){(c?a:bZ(this))?p(this).show():p(this).hide()})}}),p.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bH(a,"opacity");return c===""?"1":c}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":p.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!a||a.nodeType===3||a.nodeType===8||!a.style)return;var f,g,h,i=p.camelCase(c),j=a.style;c=p.cssProps[i]||(p.cssProps[i]=bY(j,i)),h=p.cssHooks[c]||p.cssHooks[i];if(d===b)return h&&"get"in h&&(f=h.get(a,!1,e))!==b?f:j[c];g=typeof d,g==="string"&&(f=bR.exec(d))&&(d=(f[1]+1)*f[2]+parseFloat(p.css(a,c)),g="number");if(d==null||g==="number"&&isNaN(d))return;g==="number"&&!p.cssNumber[i]&&(d+="px");if(!h||!("set"in h)||(d=h.set(a,d,e))!==b)try{j[c]=d}catch(k){}},css:function(a,c,d,e){var f,g,h,i=p.camelCase(c);return c=p.cssProps[i]||(p.cssProps[i]=bY(a.style,i)),h=p.cssHooks[c]||p.cssHooks[i],h&&"get"in h&&(f=h.get(a,!0,e)),f===b&&(f=bH(a,c)),f==="normal"&&c in bU&&(f=bU[c]),d||e!==b?(g=parseFloat(f),d||p.isNumeric(g)?g||0:f):f},swap:function(a,b,c){var d,e,f={};for(e in b)f[e]=a.style[e],a.style[e]=b[e];d=c.call(a);for(e in b)a.style[e]=f[e];return d}}),a.getComputedStyle?bH=function(b,c){var d,e,f,g,h=a.getComputedStyle(b,null),i=b.style;return h&&(d=h[c],d===""&&!p.contains(b.ownerDocument,b)&&(d=p.style(b,c)),bQ.test(d)&&bO.test(c)&&(e=i.width,f=i.minWidth,g=i.maxWidth,i.minWidth=i.maxWidth=i.width=d,d=h.width,i.width=e,i.minWidth=f,i.maxWidth=g)),d}:e.documentElement.currentStyle&&(bH=function(a,b){var c,d,e=a.currentStyle&&a.currentStyle[b],f=a.style;return e==null&&f&&f[b]&&(e=f[b]),bQ.test(e)&&!bM.test(b)&&(c=f.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":e,e=f.pixelLeft+"px",f.left=c,d&&(a.runtimeStyle.left=d)),e===""?"auto":e}),p.each(["height","width"],function(a,b){p.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth===0&&bN.test(bH(a,"display"))?p.swap(a,bT,function(){return cb(a,b,d)}):cb(a,b,d)},set:function(a,c,d){return b_(a,c,d?ca(a,b,d,p.support.boxSizing&&p.css(a,"boxSizing")==="border-box"):0)}}}),p.support.opacity||(p.cssHooks.opacity={get:function(a,b){return bL.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=p.isNumeric(b)?"alpha(opacity="+b*100+")":"",f=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&p.trim(f.replace(bK,""))===""&&c.removeAttribute){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bK.test(f)?f.replace(bK,e):f+" "+e}}),p(function(){p.support.reliableMarginRight||(p.cssHooks.marginRight={get:function(a,b){return p.swap(a,{display:"inline-block"},function(){if(b)return bH(a,"marginRight")})}}),!p.support.pixelPosition&&p.fn.position&&p.each(["top","left"],function(a,b){p.cssHooks[b]={get:function(a,c){if(c){var d=bH(a,b);return bQ.test(d)?p(a).position()[b]+"px":d}}}})}),p.expr&&p.expr.filters&&(p.expr.filters.hidden=function(a){return a.offsetWidth===0&&a.offsetHeight===0||!p.support.reliableHiddenOffsets&&(a.style&&a.style.display||bH(a,"display"))==="none"},p.expr.filters.visible=function(a){return!p.expr.filters.hidden(a)}),p.each({margin:"",padding:"",border:"Width"},function(a,b){p.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bV[d]+b]=e[d]||e[d-2]||e[0];return f}},bO.test(a)||(p.cssHooks[a+b].set=b_)});var cd=/%20/g,ce=/\[\]$/,cf=/\r?\n/g,cg=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ch=/^(?:select|textarea)/i;p.fn.extend({serialize:function(){return p.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?p.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ch.test(this.nodeName)||cg.test(this.type))}).map(function(a,b){var c=p(this).val();return c==null?null:p.isArray(c)?p.map(c,function(a,c){return{name:b.name,value:a.replace(cf,"\r\n")}}):{name:b.name,value:c.replace(cf,"\r\n")}}).get()}}),p.param=function(a,c){var d,e=[],f=function(a,b){b=p.isFunction(b)?b():b==null?"":b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=p.ajaxSettings&&p.ajaxSettings.traditional);if(p.isArray(a)||a.jquery&&!p.isPlainObject(a))p.each(a,function(){f(this.name,this.value)});else for(d in a)ci(d,a[d],c,f);return e.join("&").replace(cd,"+")};var cj,ck,cl=/#.*$/,cm=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,cn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,co=/^(?:GET|HEAD)$/,cp=/^\/\//,cq=/\?/,cr=/)<[^<]*)*<\/script>/gi,cs=/([?&])_=[^&]*/,ct=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,cu=p.fn.load,cv={},cw={},cx=["*/"]+["*"];try{cj=f.href}catch(cy){cj=e.createElement("a"),cj.href="",cj=cj.href}ck=ct.exec(cj.toLowerCase())||[],p.fn.load=function(a,c,d){if(typeof a!="string"&&cu)return cu.apply(this,arguments);if(!this.length)return this;var e,f,g,h=this,i=a.indexOf(" ");return i>=0&&(e=a.slice(i,a.length),a=a.slice(0,i)),p.isFunction(c)?(d=c,c=b):c&&typeof c=="object"&&(f="POST"),p.ajax({url:a,type:f,dataType:"html",data:c,complete:function(a,b){d&&h.each(d,g||[a.responseText,b,a])}}).done(function(a){g=arguments,h.html(e?p("
    ").append(a.replace(cr,"")).find(e):a)}),this},p.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){p.fn[b]=function(a){return this.on(b,a)}}),p.each(["get","post"],function(a,c){p[c]=function(a,d,e,f){return p.isFunction(d)&&(f=f||e,e=d,d=b),p.ajax({type:c,url:a,data:d,success:e,dataType:f})}}),p.extend({getScript:function(a,c){return p.get(a,b,c,"script")},getJSON:function(a,b,c){return p.get(a,b,c,"json")},ajaxSetup:function(a,b){return b?cB(a,p.ajaxSettings):(b=a,a=p.ajaxSettings),cB(a,b),a},ajaxSettings:{url:cj,isLocal:cn.test(ck[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":cx},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":p.parseJSON,"text xml":p.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:cz(cv),ajaxTransport:cz(cw),ajax:function(a,c){function y(a,c,f,i){var k,s,t,u,w,y=c;if(v===2)return;v=2,h&&clearTimeout(h),g=b,e=i||"",x.readyState=a>0?4:0,f&&(u=cC(l,x,f));if(a>=200&&a<300||a===304)l.ifModified&&(w=x.getResponseHeader("Last-Modified"),w&&(p.lastModified[d]=w),w=x.getResponseHeader("Etag"),w&&(p.etag[d]=w)),a===304?(y="notmodified",k=!0):(k=cD(l,u),y=k.state,s=k.data,t=k.error,k=!t);else{t=y;if(!y||a)y="error",a<0&&(a=0)}x.status=a,x.statusText=""+(c||y),k?o.resolveWith(m,[s,y,x]):o.rejectWith(m,[x,y,t]),x.statusCode(r),r=b,j&&n.trigger("ajax"+(k?"Success":"Error"),[x,l,k?s:t]),q.fireWith(m,[x,y]),j&&(n.trigger("ajaxComplete",[x,l]),--p.active||p.event.trigger("ajaxStop"))}typeof a=="object"&&(c=a,a=b),c=c||{};var d,e,f,g,h,i,j,k,l=p.ajaxSetup({},c),m=l.context||l,n=m!==l&&(m.nodeType||m instanceof p)?p(m):p.event,o=p.Deferred(),q=p.Callbacks("once memory"),r=l.statusCode||{},t={},u={},v=0,w="canceled",x={readyState:0,setRequestHeader:function(a,b){if(!v){var c=a.toLowerCase();a=u[c]=u[c]||a,t[a]=b}return this},getAllResponseHeaders:function(){return v===2?e:null},getResponseHeader:function(a){var c;if(v===2){if(!f){f={};while(c=cm.exec(e))f[c[1].toLowerCase()]=c[2]}c=f[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){return v||(l.mimeType=a),this},abort:function(a){return a=a||w,g&&g.abort(a),y(0,a),this}};o.promise(x),x.success=x.done,x.error=x.fail,x.complete=q.add,x.statusCode=function(a){if(a){var b;if(v<2)for(b in a)r[b]=[r[b],a[b]];else b=a[x.status],x.always(b)}return this},l.url=((a||l.url)+"").replace(cl,"").replace(cp,ck[1]+"//"),l.dataTypes=p.trim(l.dataType||"*").toLowerCase().split(s),l.crossDomain==null&&(i=ct.exec(l.url.toLowerCase()),l.crossDomain=!(!i||i[1]==ck[1]&&i[2]==ck[2]&&(i[3]||(i[1]==="http:"?80:443))==(ck[3]||(ck[1]==="http:"?80:443)))),l.data&&l.processData&&typeof l.data!="string"&&(l.data=p.param(l.data,l.traditional)),cA(cv,l,c,x);if(v===2)return x;j=l.global,l.type=l.type.toUpperCase(),l.hasContent=!co.test(l.type),j&&p.active++===0&&p.event.trigger("ajaxStart");if(!l.hasContent){l.data&&(l.url+=(cq.test(l.url)?"&":"?")+l.data,delete l.data),d=l.url;if(l.cache===!1){var z=p.now(),A=l.url.replace(cs,"$1_="+z);l.url=A+(A===l.url?(cq.test(l.url)?"&":"?")+"_="+z:"")}}(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&x.setRequestHeader("Content-Type",l.contentType),l.ifModified&&(d=d||l.url,p.lastModified[d]&&x.setRequestHeader("If-Modified-Since",p.lastModified[d]),p.etag[d]&&x.setRequestHeader("If-None-Match",p.etag[d])),x.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+(l.dataTypes[0]!=="*"?", "+cx+"; q=0.01":""):l.accepts["*"]);for(k in l.headers)x.setRequestHeader(k,l.headers[k]);if(!l.beforeSend||l.beforeSend.call(m,x,l)!==!1&&v!==2){w="abort";for(k in{success:1,error:1,complete:1})x[k](l[k]);g=cA(cw,l,c,x);if(!g)y(-1,"No Transport");else{x.readyState=1,j&&n.trigger("ajaxSend",[x,l]),l.async&&l.timeout>0&&(h=setTimeout(function(){x.abort("timeout")},l.timeout));try{v=1,g.send(t,y)}catch(B){if(v<2)y(-1,B);else throw B}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var cE=[],cF=/\?/,cG=/(=)\?(?=&|$)|\?\?/,cH=p.now();p.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=cE.pop()||p.expando+"_"+cH++;return this[a]=!0,a}}),p.ajaxPrefilter("json jsonp",function(c,d,e){var f,g,h,i=c.data,j=c.url,k=c.jsonp!==!1,l=k&&cG.test(j),m=k&&!l&&typeof i=="string"&&!(c.contentType||"").indexOf("application/x-www-form-urlencoded")&&cG.test(i);if(c.dataTypes[0]==="jsonp"||l||m)return f=c.jsonpCallback=p.isFunction(c.jsonpCallback)?c.jsonpCallback():c.jsonpCallback,g=a[f],l?c.url=j.replace(cG,"$1"+f):m?c.data=i.replace(cG,"$1"+f):k&&(c.url+=(cF.test(j)?"&":"?")+c.jsonp+"="+f),c.converters["script json"]=function(){return h||p.error(f+" was not called"),h[0]},c.dataTypes[0]="json",a[f]=function(){h=arguments},e.always(function(){a[f]=g,c[f]&&(c.jsonpCallback=d.jsonpCallback,cE.push(f)),h&&p.isFunction(g)&&g(h[0]),h=g=b}),"script"}),p.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){return p.globalEval(a),a}}}),p.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),p.ajaxTransport("script",function(a){if(a.crossDomain){var c,d=e.head||e.getElementsByTagName("head")[0]||e.documentElement;return{send:function(f,g){c=e.createElement("script"),c.async="async",a.scriptCharset&&(c.charset=a.scriptCharset),c.src=a.url,c.onload=c.onreadystatechange=function(a,e){if(e||!c.readyState||/loaded|complete/.test(c.readyState))c.onload=c.onreadystatechange=null,d&&c.parentNode&&d.removeChild(c),c=b,e||g(200,"success")},d.insertBefore(c,d.firstChild)},abort:function(){c&&c.onload(0,1)}}}});var cI,cJ=a.ActiveXObject?function(){for(var a in cI)cI[a](0,1)}:!1,cK=0;p.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cL()||cM()}:cL,function(a){p.extend(p.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(p.ajaxSettings.xhr()),p.support.ajax&&p.ajaxTransport(function(c){if(!c.crossDomain||p.support.cors){var d;return{send:function(e,f){var g,h,i=c.xhr();c.username?i.open(c.type,c.url,c.async,c.username,c.password):i.open(c.type,c.url,c.async);if(c.xhrFields)for(h in c.xhrFields)i[h]=c.xhrFields[h];c.mimeType&&i.overrideMimeType&&i.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(h in e)i.setRequestHeader(h,e[h])}catch(j){}i.send(c.hasContent&&c.data||null),d=function(a,e){var h,j,k,l,m;try{if(d&&(e||i.readyState===4)){d=b,g&&(i.onreadystatechange=p.noop,cJ&&delete cI[g]);if(e)i.readyState!==4&&i.abort();else{h=i.status,k=i.getAllResponseHeaders(),l={},m=i.responseXML,m&&m.documentElement&&(l.xml=m);try{l.text=i.responseText}catch(a){}try{j=i.statusText}catch(n){j=""}!h&&c.isLocal&&!c.crossDomain?h=l.text?200:404:h===1223&&(h=204)}}}catch(o){e||f(-1,o)}l&&f(h,j,l,k)},c.async?i.readyState===4?setTimeout(d,0):(g=++cK,cJ&&(cI||(cI={},p(a).unload(cJ)),cI[g]=d),i.onreadystatechange=d):d()},abort:function(){d&&d(0,1)}}}});var cN,cO,cP=/^(?:toggle|show|hide)$/,cQ=new RegExp("^(?:([-+])=|)("+q+")([a-z%]*)$","i"),cR=/queueHooks$/,cS=[cY],cT={"*":[function(a,b){var c,d,e,f=this.createTween(a,b),g=cQ.exec(b),h=f.cur(),i=+h||0,j=1;if(g){c=+g[2],d=g[3]||(p.cssNumber[a]?"":"px");if(d!=="px"&&i){i=p.css(f.elem,a,!0)||c||1;do e=j=j||".5",i=i/j,p.style(f.elem,a,i+d),j=f.cur()/h;while(j!==1&&j!==e)}f.unit=d,f.start=i,f.end=g[1]?i+(g[1]+1)*c:c}return f}]};p.Animation=p.extend(cW,{tweener:function(a,b){p.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");var c,d=0,e=a.length;for(;d-1,j={},k={},l,m;i?(k=e.position(),l=k.top,m=k.left):(l=parseFloat(g)||0,m=parseFloat(h)||0),p.isFunction(b)&&(b=b.call(a,c,f)),b.top!=null&&(j.top=b.top-f.top+l),b.left!=null&&(j.left=b.left-f.left+m),"using"in b?b.using.call(a,j):e.css(j)}},p.fn.extend({position:function(){if(!this[0])return;var a=this[0],b=this.offsetParent(),c=this.offset(),d=c_.test(b[0].nodeName)?{top:0,left:0}:b.offset();return c.top-=parseFloat(p.css(a,"marginTop"))||0,c.left-=parseFloat(p.css(a,"marginLeft"))||0,d.top+=parseFloat(p.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(p.css(b[0],"borderLeftWidth"))||0,{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||e.body;while(a&&!c_.test(a.nodeName)&&p.css(a,"position")==="static")a=a.offsetParent;return a||e.body})}}),p.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);p.fn[a]=function(e){return p.access(this,function(a,e,f){var g=da(a);if(f===b)return g?c in g?g[c]:g.document.documentElement[e]:a[e];g?g.scrollTo(d?p(g).scrollLeft():f,d?f:p(g).scrollTop()):a[e]=f},a,e,arguments.length,null)}}),p.each({Height:"height",Width:"width"},function(a,c){p.each({padding:"inner"+a,content:c,"":"outer"+a},function(d,e){p.fn[e]=function(e,f){var g=arguments.length&&(d||typeof e!="boolean"),h=d||(e===!0||f===!0?"margin":"border");return p.access(this,function(c,d,e){var f;return p.isWindow(c)?c.document.documentElement["client"+a]:c.nodeType===9?(f=c.documentElement,Math.max(c.body["scroll"+a],f["scroll"+a],c.body["offset"+a],f["offset"+a],f["client"+a])):e===b?p.css(c,d,e,h):p.style(c,d,e,h)},c,g?e:b,g,null)}})}),a.jQuery=a.$=p,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return p})})(window); \ No newline at end of file -- cgit v1.2.3-54-g00ecf From 3769994c4e58197fbb063c9e9bdc184af66edaf9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 7 Sep 2012 09:34:11 -0500 Subject: Update downloads page for September release Signed-off-by: Dan McGee --- templates/public/download.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/public/download.html b/templates/public/download.html index 79a82f8b..63f6d79c 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -7,7 +7,7 @@ {% block navbarclass %}anb-download{% endblock %} {% block content %} -{% with version="2012.08.04" kernel_version="3.4.7" %} +{% with version="2012.09.07" kernel_version="3.5.3" %}

    Arch Linux Downloads

    -- cgit v1.2.3-54-g00ecf From f221426a3286b9c9cfcc63a9caea861be9e99d02 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 8 Sep 2012 10:07:30 -0500 Subject: Remove 'new' markers from home page These things aren't so new anymore. Signed-off-by: Dan McGee --- templates/public/index.html | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/templates/public/index.html b/templates/public/index.html index 0b97baf8..60ae442d 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -156,8 +156,7 @@

    Tools

  • Mirror Status
  • Differences Reports - New
  • + title="See differences in packages between available architectures">Differences Reports

    Development

    @@ -176,16 +175,14 @@

    Development

  • Releng Testbuild Feedback
  • Visualizations - New
  • + title="View visualizations">Visualizations

    More Resources

    • Signing Master Keys - New
    • + title="Package/Database signing master keys">Signing Master Keys
    • Press Coverage
    • Logos & Artwork
    • -- cgit v1.2.3-54-g00ecf From 585b53a52a441761690ef81cd293597f38fa8a2f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 8 Sep 2012 11:08:36 -0500 Subject: Add structured data markup from schema.org to package details This might help out search engines show more helpful blurbs for our package details pages. Tested using the Google Rich Snippets Testing Tool at http://www.google.com/webmasters/tools/richsnippets. Signed-off-by: Dan McGee --- templates/packages/details.html | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/templates/packages/details.html b/templates/packages/details.html index 9e898b7f..5a5598a0 100644 --- a/templates/packages/details.html +++ b/templates/packages/details.html @@ -72,6 +72,17 @@

      Versions Elsewhere

      {% endif %}{% endwith %}
    +
    + + + + + + + +
    + +
    @@ -102,10 +113,10 @@

    Versions Elsewhere

    {% endifequal %} - + - @@ -186,9 +197,9 @@

    Versions Elsewhere

    {{ flag_request.message|linebreaksbr|default:"{no message}" }}
    {% endif %}{% endwith %}{% endif %}
    Architecture:
    Description:{{ pkg.pkgdesc|default:"" }}{{ pkg.pkgdesc|default:"" }}
    Upstream URL:{% if pkg.url %}{% if pkg.url %}{% endif %}
    License(s):
    +
    - {% with pkg.get_depends as deps %}

    @@ -198,7 +209,6 @@

    {% endif %}

    {% endwith %} - {% with pkg.get_requiredby as rqdby %}

    @@ -208,7 +218,6 @@

    {% endif %}

    {% endwith %} -

    Package Contents

    @@ -218,9 +227,7 @@

    View the file list for {{ pkg.pkgname }}

    - - {% load cdn %}{% jquery %} -- cgit v1.2.3-54-g00ecf From 01a64613eea1ccf5bdb1c7c9b563c9c9597bab74 Mon Sep 17 00:00:00 2001 From: Florian Pritz Date: Sat, 8 Sep 2012 16:59:29 +0200 Subject: local_settings.py: Add sqlite example Sqlite should be sufficient for testing so make it the default. Signed-off-by: Florian Pritz --- local_settings.py.example | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/local_settings.py.example b/local_settings.py.example index b8407d3e..df141521 100644 --- a/local_settings.py.example +++ b/local_settings.py.example @@ -14,14 +14,22 @@ ADMINS = ( ) ## PostgreSQL Database settings +#DATABASES = { +# 'default': { +# 'ENGINE' : 'django.db.backends.postgresql_psycopg2', +# 'NAME' : 'archlinux', +# 'USER' : 'archlinux', +# 'PASSWORD': 'archlinux', +# 'HOST' : '', +# 'PORT' : '', +# }, +#} + +## Sqlite Database settings DATABASES = { 'default': { - 'ENGINE' : 'django.db.backends.postgresql_psycopg2', - 'NAME' : 'archlinux', - 'USER' : 'archlinux', - 'PASSWORD': 'archlinux', - 'HOST' : '', - 'PORT' : '', + 'ENGINE' : 'django.db.backends.sqlite3', + 'NAME' : 'database.db', }, } -- cgit v1.2.3-54-g00ecf From f2f00b3c0474c4776e7a7f0e58162dc67ce2ca18 Mon Sep 17 00:00:00 2001 From: Florian Pritz Date: Sat, 8 Sep 2012 17:08:21 +0200 Subject: p/v/flag: Add reply-to to out-of-date notifications Signed-off-by: Florian Pritz Signed-off-by: Dan McGee --- packages/views/flag.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/views/flag.py b/packages/views/flag.py index 33cec006..d7302f72 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -3,7 +3,7 @@ from django import forms from django.conf import settings from django.contrib.auth.decorators import permission_required -from django.core.mail import send_mail +from django.core.mail import send_mail, EmailMessage from django.db import transaction from django.shortcuts import get_object_or_404, redirect, render from django.template import loader, Context @@ -122,11 +122,13 @@ def perform_updates(): 'pkg': pkg, 'packages': flagged_pkgs, }) - send_mail(subject, + msg = EmailMessage(subject, tmpl.render(ctx), 'Arch Website Notification ', toemail, - fail_silently=True) + headers={"Reply-To": email } + ) + msg.send(fail_silently=True) return redirect('package-flag-confirmed', name=name, repo=repo, arch=arch) -- cgit v1.2.3-54-g00ecf From 59c353e22c5a9d1fda347e82f0734cf78bc7d387 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 15 Sep 2012 08:52:34 -0500 Subject: Standardize spelling of 'todo list' We use a space everywhere but a few places; fix the exceptions. Signed-off-by: Dan McGee --- templates/todolists/view.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/todolists/view.html b/templates/todolists/view.html index 35bc9446..c22bfecf 100644 --- a/templates/todolists/view.html +++ b/templates/todolists/view.html @@ -29,10 +29,10 @@

    Todo List: {{ list.name }}

  • {{ svn_root }}
  • {% endfor %} -

    {{ list.packages|length }} total todolist package{{ list.packages|pluralize }} found.

    +

    {{ list.packages|length }} total todo list package{{ list.packages|pluralize }} found.

    -

    Filter Todolist Packages

    +

    Filter Todo List Packages

    Select filter criteria @@ -50,7 +50,7 @@

    Filter Todolist Packages

    -
    {{ list.packages|length }} todolist packages displayed.
    +
    {{ list.packages|length }} todo list packages displayed.
    -- cgit v1.2.3-54-g00ecf From dfbf7cdd4c273f2b1f3c1c79b97149d37e63e028 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 15 Sep 2012 09:13:58 -0500 Subject: Make todolist filtering functions more generic This will allow us to use them elsewhere in a future commit. Signed-off-by: Dan McGee --- sitestatic/archweb.js | 14 ++++++++------ templates/todolists/view.html | 7 ++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/sitestatic/archweb.js b/sitestatic/archweb.js index c274a675..783f75c6 100644 --- a/sitestatic/archweb.js +++ b/sitestatic/archweb.js @@ -293,17 +293,17 @@ function todolist_flag() { return false; } -function filter_todolist() { +function filter_pkgs_list(filter_ele, tbody_ele) { /* start with all rows, and then remove ones we shouldn't show */ - var rows = $('#dev-todo-pkglist tbody').children(); + var rows = $(tbody_ele).children(); var all_rows = rows; /* apply the filters, cheaper ones first */ if ($('#id_mine_only').is(':checked')) { rows = rows.filter('.mine'); } /* apply arch and repo filters */ - $('#todolist_filter .arch_filter').add( - '#todolist_filter .repo_filter').each(function() { + $(filter_ele + ' .arch_filter').add( + filter_ele + ' .repo_filter').each(function() { if (!$(this).is(':checked')) { rows = rows.not('.' + $(this).val()); } @@ -319,10 +319,12 @@ function filter_todolist() { /* make sure we update the odd/even styling from sorting */ $('.results').trigger('applyWidgets', [false]); } -function filter_todolist_reset() { +function filter_pkgs_reset(callback) { $('#id_incomplete').removeAttr('checked'); $('#id_mine_only').removeAttr('checked'); - filter_todolist(); + $('.arch_filter').attr('checked', 'checked'); + $('.repo_filter').attr('checked', 'checked'); + callback(); } /* signoffs.html */ diff --git a/templates/todolists/view.html b/templates/todolists/view.html index c22bfecf..eff81aaf 100644 --- a/templates/todolists/view.html +++ b/templates/todolists/view.html @@ -106,10 +106,11 @@

    Filter Todo List Packages

    headers: { 5: { sorter: 'todostatus' } } }); $('a.status-link').click(todolist_flag); - $('#todolist_filter input').change(filter_todolist); - $('#criteria_reset').click(filter_todolist_reset); + filter_func = function() { filter_pkgs_list('#todolist_filter', '#dev-todo-pkglist tbody'); }; + $('#todolist_filter input').change(filter_func); + $('#criteria_reset').click(function() { filter_pkgs_reset(filter_func); }); // fire function on page load to ensure the current form selections take effect - filter_todolist(); + filter_func(); }); {% endblock %} -- cgit v1.2.3-54-g00ecf From a2034fc80d4e73816502537f8dfe864ab4ef8db3 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 15 Sep 2012 09:14:36 -0500 Subject: Add JS-based filtering to the developer reports This can use the todolist filtering functions we made more generic in a previous commit. Signed-off-by: Dan McGee --- devel/views.py | 4 ++++ templates/devel/packages.html | 30 ++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/devel/views.py b/devel/views.py index 5406974e..23ff9f74 100644 --- a/devel/views.py +++ b/devel/views.py @@ -271,11 +271,15 @@ def report(request, report_name, username=None): else: raise Http404 + arches = set(pkg.arch for pkg in packages) + repos = set(pkg.repo for pkg in packages) context = { 'all_maintainers': maints, 'title': title, 'maintainer': user, 'packages': packages, + 'arches': sorted(arches), + 'repos': sorted(repos), 'column_names': names, 'column_attrs': attrs, } diff --git a/templates/devel/packages.html b/templates/devel/packages.html index ac368124..4e1381ab 100644 --- a/templates/devel/packages.html +++ b/templates/devel/packages.html @@ -13,7 +13,28 @@

    {{ title }}{% if maintainer %}, {% if maintainer %}This report only includes packages maintained by {{ maintainer.get_full_name }} ({{ maintainer.username }}).{% endif %}

    - + +
    +

    Filter Packages

    +
    +
    + Select filter criteria + {% for arch in arches %} +
    +
    + {% endfor %} + {% for repo in repos %} +
    +
    + {% endfor %} +
    +
    +
    {{ packages|length }} packages displayed.
    +
    + +
    + +
    @@ -31,7 +52,7 @@

    {{ title }}{% if maintainer %},

    {% for pkg in packages %} - + @@ -57,6 +78,11 @@

    {{ title }}{% if maintainer %}, {% endblock %} -- cgit v1.2.3-54-g00ecf From b379981938db8561bd1ba856bbd7b2dfbe98dfaf Mon Sep 17 00:00:00 2001 From: Florian Pritz Date: Fri, 14 Sep 2012 21:41:20 +0200 Subject: donate.html: Add kartenzia.de Signed-off-by: Florian Pritz --- sitestatic/kartenzia_button.jpg | Bin 0 -> 40007 bytes templates/public/donate.html | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 sitestatic/kartenzia_button.jpg diff --git a/sitestatic/kartenzia_button.jpg b/sitestatic/kartenzia_button.jpg new file mode 100644 index 00000000..26c86a51 Binary files /dev/null and b/sitestatic/kartenzia_button.jpg differ diff --git a/templates/public/donate.html b/templates/public/donate.html index b1d52f88..202c5351 100644 --- a/templates/public/donate.html +++ b/templates/public/donate.html @@ -65,6 +65,12 @@

    Commercial sponsors and contributions

    title="AirVM.com - Your Green Technology Partner"> +

    We would also like to thank Kartenzia for sponsoring a dedicated Arch Linux Server. + Kartenzia.de is a new Start-Up based in Germany and specializes in + eco-friendly invitations like Weihnachtskarten.

    + + +

    Past donors

    -- cgit v1.2.3-54-g00ecf From 47ec2603ba50099be7ce45920e6411a3c2939481 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 15 Sep 2012 09:20:28 -0500 Subject: Switch to Kartenzia PNG logo Signed-off-by: Dan McGee --- sitestatic/kartenzia_button.jpg | Bin 40007 -> 0 bytes sitestatic/kartenzia_button.png | Bin 0 -> 24726 bytes templates/public/donate.html | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 sitestatic/kartenzia_button.jpg create mode 100644 sitestatic/kartenzia_button.png diff --git a/sitestatic/kartenzia_button.jpg b/sitestatic/kartenzia_button.jpg deleted file mode 100644 index 26c86a51..00000000 Binary files a/sitestatic/kartenzia_button.jpg and /dev/null differ diff --git a/sitestatic/kartenzia_button.png b/sitestatic/kartenzia_button.png new file mode 100644 index 00000000..3621e166 Binary files /dev/null and b/sitestatic/kartenzia_button.png differ diff --git a/templates/public/donate.html b/templates/public/donate.html index 202c5351..265a0085 100644 --- a/templates/public/donate.html +++ b/templates/public/donate.html @@ -69,7 +69,7 @@

    Commercial sponsors and contributions

    Kartenzia.de is a new Start-Up based in Germany and specializes in eco-friendly invitations like Weihnachtskarten.

    - +

    Past donors

    -- cgit v1.2.3-54-g00ecf From 9e0df2b2873533fd3faa6525c0f925c70acb2847 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 15 Sep 2012 09:22:14 -0500 Subject: Reduce Kartenzia button size with OptiPNG Signed-off-by: Dan McGee --- sitestatic/kartenzia_button.png | Bin 24726 -> 15959 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/sitestatic/kartenzia_button.png b/sitestatic/kartenzia_button.png index 3621e166..d4a3ae96 100644 Binary files a/sitestatic/kartenzia_button.png and b/sitestatic/kartenzia_button.png differ -- cgit v1.2.3-54-g00ecf From 97595b7a26f09f89905e893b8be56b1424d0584b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 15 Sep 2012 09:44:55 -0500 Subject: Add structured data markup from schema.org to news items We use the 'Article' type since this isn't print media. Signed-off-by: Dan McGee --- templates/news/view.html | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/templates/news/view.html b/templates/news/view.html index 7788dece..8a4d8122 100644 --- a/templates/news/view.html +++ b/templates/news/view.html @@ -3,9 +3,19 @@ {% block title %}Arch Linux - News: {{ news.title }}{% endblock %} {% block content %} -
    +
    -

    News: {{ news.title }}

    +

    {{ news.title }}

    + + + + + +
    + +
    {% if perms.news.change_news %}
      -- cgit v1.2.3-54-g00ecf From 10f5c27bf8124a3ccffb94930283b5062ad96cce Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 10 Sep 2012 08:27:23 -0500 Subject: Remove now unnecessary import Signed-off-by: Dan McGee --- packages/views/flag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/views/flag.py b/packages/views/flag.py index d7302f72..dadadd19 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -3,7 +3,7 @@ from django import forms from django.conf import settings from django.contrib.auth.decorators import permission_required -from django.core.mail import send_mail, EmailMessage +from django.core.mail import EmailMessage from django.db import transaction from django.shortcuts import get_object_or_404, redirect, render from django.template import loader, Context -- cgit v1.2.3-54-g00ecf From 6c8413172506b5cce4f39e17f09803efea753be7 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 17 Sep 2012 20:47:33 -0500 Subject: More structured data markup for news articles Signed-off-by: Dan McGee --- templates/news/view.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/templates/news/view.html b/templates/news/view.html index 8a4d8122..445f0398 100644 --- a/templates/news/view.html +++ b/templates/news/view.html @@ -4,12 +4,12 @@ {% block content %}
      -

      {{ news.title }}

      + @@ -28,7 +28,6 @@

      {{ news.title }}

      -
      {{ news.content|markdown }}
      - +
      {{ news.content|markdown }}
      {% endblock %} -- cgit v1.2.3-54-g00ecf From cf5b31952e119d7fe7ad98cd739aa7cc564fe07b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 17 Sep 2012 21:15:54 -0500 Subject: Minor updates to Kartenzia linkage And add some transparent rounded corners to the logo so it fits in better with the rest of our sponsor buttons. Signed-off-by: Dan McGee --- sitestatic/kartenzia_button.png | Bin 15959 -> 16920 bytes templates/public/donate.html | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sitestatic/kartenzia_button.png b/sitestatic/kartenzia_button.png index d4a3ae96..c156fc86 100644 Binary files a/sitestatic/kartenzia_button.png and b/sitestatic/kartenzia_button.png differ diff --git a/templates/public/donate.html b/templates/public/donate.html index 265a0085..988ec50c 100644 --- a/templates/public/donate.html +++ b/templates/public/donate.html @@ -67,9 +67,9 @@

      Commercial sponsors and contributions

      We would also like to thank Kartenzia for sponsoring a dedicated Arch Linux Server. Kartenzia.de is a new Start-Up based in Germany and specializes in - eco-friendly invitations like Weihnachtskarten.

      + eco-friendly invitations like Weihnachten.

      - +

      Past donors

      -- cgit v1.2.3-54-g00ecf From c76e5c768687394b8022883d01edf85dc3c30e7f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 17 Sep 2012 21:42:48 -0500 Subject: Sort package list before inserting it into the database FS#30323. This will take some time to propagate to all existing packages, but all new and updated packages will start getting filelists in the right order. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index af0a2dc0..ac745092 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -303,7 +303,10 @@ def populate_files(dbpkg, repopkg, force=False): logger.info("adding %d files for package %s", len(repopkg.files), dbpkg.pkgname) pkg_files = [] - for f in repopkg.files: + # sort in normal alpha-order that pacman uses, rather than makepkg's + # default breadth-first, directory-first ordering + files = sorted(repopkg.files) + for f in files: if '/' in f: dirname, filename = f.rsplit('/', 1) dirname += '/' -- cgit v1.2.3-54-g00ecf From e9c4985538c067a09a186967f77c5395fb60675b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 18 Sep 2012 22:10:54 -0500 Subject: Sort mirrorlist by English country name, not ISO code Fixes FS#31503. Signed-off-by: Dan McGee --- mirrors/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mirrors/views.py b/mirrors/views.py index 2c2577f4..11719223 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -134,7 +134,7 @@ def find_mirrors(request, countries=None, protocols=None, use_status=False, urls = qset if not use_status: - sort_key = attrgetter('real_country', 'mirror.name', 'url') + sort_key = attrgetter('real_country.name', 'mirror.name', 'url') urls = sorted(urls, key=sort_key) template = 'mirrors/mirrorlist.txt' else: -- cgit v1.2.3-54-g00ecf From 2fd5c74004c4b42bb363007d565ac9cb0890818c Mon Sep 17 00:00:00 2001 From: Thomas Bächler Date: Thu, 20 Sep 2012 15:51:55 +0200 Subject: More minor updates to Kartenzia linkage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last update broke one of the links. Signed-off-by: Thomas Bächler Signed-off-by: Dan McGee --- templates/public/donate.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/public/donate.html b/templates/public/donate.html index 988ec50c..b47683e7 100644 --- a/templates/public/donate.html +++ b/templates/public/donate.html @@ -67,7 +67,7 @@

      Commercial sponsors and contributions

      We would also like to thank Kartenzia for sponsoring a dedicated Arch Linux Server. Kartenzia.de is a new Start-Up based in Germany and specializes in - eco-friendly invitations like Weihnachten.

      + eco-friendly invitations like Weihnachten.

      -- cgit v1.2.3-54-g00ecf From 33bfba71042a55ff57115e833aabfed9852e80f4 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 20 Sep 2012 09:31:27 -0500 Subject: Defer content and other text items when building news sitemap We can get away with not retrieving these larger pieces of data when building the news sitemap. Signed-off-by: Dan McGee --- sitemaps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sitemaps.py b/sitemaps.py index 97ab43c0..b53c990b 100644 --- a/sitemaps.py +++ b/sitemaps.py @@ -66,7 +66,7 @@ def __init__(self): self.one_week_ago = now - timedelta(days=7) def items(self): - return News.objects.all().order_by() + return News.objects.all().defer('content', 'guid', 'title').order_by() def lastmod(self, obj): return obj.last_modified -- cgit v1.2.3-54-g00ecf From 7d5cfe45d52c4dbd2f431f0edcafc9936b740ab2 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 20 Sep 2012 09:32:42 -0500 Subject: Explicitly close the database connection in reporead This is the cause of these warnings showing up in the PostgreSQL log: LOG: unexpected EOF on client connection with an open transaction All management commands are guilty of this as they do not clean up and close the connection when they exit, unlike the standard web request cycle. Other commands should probably be updated as well, but for now, this is the biggest culprit. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 1 + 1 file changed, 1 insertion(+) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index ac745092..ce5c8cb7 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -584,6 +584,7 @@ def read_repo(primary_arch, repo_file, options): else: db_update(arch, repo, packages_arches[arch], force) logger.info('Finished database updates for %s.', repo_file) + connection.close() return 0 # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 89c6ffc95cb1d5fe4bd2534562ca732d727a8686 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 20 Sep 2012 09:34:46 -0500 Subject: chmod -x reporead_inotify.py Signed-off-by: Dan McGee --- devel/management/commands/reporead_inotify.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 devel/management/commands/reporead_inotify.py diff --git a/devel/management/commands/reporead_inotify.py b/devel/management/commands/reporead_inotify.py old mode 100755 new mode 100644 -- cgit v1.2.3-54-g00ecf From ddaab159ad5e8735fae7f8d29301181009478d1b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 24 Sep 2012 20:02:15 -0500 Subject: Add stock Bootstrap typeahead JS files Signed-off-by: Dan McGee --- sitestatic/bootstrap-typeahead.js | 300 ++++++++++++++++++++++++++++++++++ sitestatic/bootstrap-typeahead.min.js | 1 + 2 files changed, 301 insertions(+) create mode 100644 sitestatic/bootstrap-typeahead.js create mode 100644 sitestatic/bootstrap-typeahead.min.js diff --git a/sitestatic/bootstrap-typeahead.js b/sitestatic/bootstrap-typeahead.js new file mode 100644 index 00000000..c2ccdea2 --- /dev/null +++ b/sitestatic/bootstrap-typeahead.js @@ -0,0 +1,300 @@ +/* ============================================================= + * bootstrap-typeahead.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#typeahead + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function($){ + + "use strict"; // jshint ;_; + + + /* TYPEAHEAD PUBLIC CLASS DEFINITION + * ================================= */ + + var Typeahead = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.typeahead.defaults, options) + this.matcher = this.options.matcher || this.matcher + this.sorter = this.options.sorter || this.sorter + this.highlighter = this.options.highlighter || this.highlighter + this.updater = this.options.updater || this.updater + this.$menu = $(this.options.menu).appendTo('body') + this.source = this.options.source + this.shown = false + this.listen() + } + + Typeahead.prototype = { + + constructor: Typeahead + + , select: function () { + var val = this.$menu.find('.active').attr('data-value') + this.$element + .val(this.updater(val)) + .change() + return this.hide() + } + + , updater: function (item) { + return item + } + + , show: function () { + var pos = $.extend({}, this.$element.offset(), { + height: this.$element[0].offsetHeight + }) + + this.$menu.css({ + top: pos.top + pos.height + , left: pos.left + }) + + this.$menu.show() + this.shown = true + return this + } + + , hide: function () { + this.$menu.hide() + this.shown = false + return this + } + + , lookup: function (event) { + var items + + this.query = this.$element.val() + + if (!this.query || this.query.length < this.options.minLength) { + return this.shown ? this.hide() : this + } + + items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source + + return items ? this.process(items) : this + } + + , process: function (items) { + var that = this + + items = $.grep(items, function (item) { + return that.matcher(item) + }) + + items = this.sorter(items) + + if (!items.length) { + return this.shown ? this.hide() : this + } + + return this.render(items.slice(0, this.options.items)).show() + } + + , matcher: function (item) { + return ~item.toLowerCase().indexOf(this.query.toLowerCase()) + } + + , sorter: function (items) { + var beginswith = [] + , caseSensitive = [] + , caseInsensitive = [] + , item + + while (item = items.shift()) { + if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) + else if (~item.indexOf(this.query)) caseSensitive.push(item) + else caseInsensitive.push(item) + } + + return beginswith.concat(caseSensitive, caseInsensitive) + } + + , highlighter: function (item) { + var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&') + return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { + return '' + match + '' + }) + } + + , render: function (items) { + var that = this + + items = $(items).map(function (i, item) { + i = $(that.options.item).attr('data-value', item) + i.find('a').html(that.highlighter(item)) + return i[0] + }) + + items.first().addClass('active') + this.$menu.html(items) + return this + } + + , next: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , next = active.next() + + if (!next.length) { + next = $(this.$menu.find('li')[0]) + } + + next.addClass('active') + } + + , prev: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , prev = active.prev() + + if (!prev.length) { + prev = this.$menu.find('li').last() + } + + prev.addClass('active') + } + + , listen: function () { + this.$element + .on('blur', $.proxy(this.blur, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)) + + if ($.browser.chrome || $.browser.webkit || $.browser.msie) { + this.$element.on('keydown', $.proxy(this.keydown, this)) + } + + this.$menu + .on('click', $.proxy(this.click, this)) + .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) + } + + , move: function (e) { + if (!this.shown) return + + switch(e.keyCode) { + case 9: // tab + case 13: // enter + case 27: // escape + e.preventDefault() + break + + case 38: // up arrow + e.preventDefault() + this.prev() + break + + case 40: // down arrow + e.preventDefault() + this.next() + break + } + + e.stopPropagation() + } + + , keydown: function (e) { + this.suppressKeyPressRepeat = !~$.inArray(e.keyCode, [40,38,9,13,27]) + this.move(e) + } + + , keypress: function (e) { + if (this.suppressKeyPressRepeat) return + this.move(e) + } + + , keyup: function (e) { + switch(e.keyCode) { + case 40: // down arrow + case 38: // up arrow + break + + case 9: // tab + case 13: // enter + if (!this.shown) return + this.select() + break + + case 27: // escape + if (!this.shown) return + this.hide() + break + + default: + this.lookup() + } + + e.stopPropagation() + e.preventDefault() + } + + , blur: function (e) { + var that = this + setTimeout(function () { that.hide() }, 150) + } + + , click: function (e) { + e.stopPropagation() + e.preventDefault() + this.select() + } + + , mouseenter: function (e) { + this.$menu.find('.active').removeClass('active') + $(e.currentTarget).addClass('active') + } + + } + + + /* TYPEAHEAD PLUGIN DEFINITION + * =========================== */ + + $.fn.typeahead = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('typeahead') + , options = typeof option == 'object' && option + if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.typeahead.defaults = { + source: [] + , items: 8 + , menu: '' + , item: '
    • ' + , minLength: 1 + } + + $.fn.typeahead.Constructor = Typeahead + + + /* TYPEAHEAD DATA-API + * ================== */ + + $(function () { + $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { + var $this = $(this) + if ($this.data('typeahead')) return + e.preventDefault() + $this.typeahead($this.data()) + }) + }) + +}(window.jQuery); diff --git a/sitestatic/bootstrap-typeahead.min.js b/sitestatic/bootstrap-typeahead.min.js new file mode 100644 index 00000000..a5178904 --- /dev/null +++ b/sitestatic/bootstrap-typeahead.min.js @@ -0,0 +1 @@ +!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.$menu=e(this.options.menu).appendTo("body"),this.source=this.options.source,this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.offset(),{height:this.$element[0].offsetHeight});return this.$menu.css({top:t.top+t.height,left:t.left}),this.$menu.show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length"+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),(e.browser.chrome||e.browser.webkit||e.browser.msie)&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this))},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=!~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},blur:function(e){var t=this;setTimeout(function(){t.hide()},150)},click:function(e){e.stopPropagation(),e.preventDefault(),this.select()},mouseenter:function(t){this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")}},e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
    • ',minLength:1},e.fn.typeahead.Constructor=t,e(function(){e("body").on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;t.preventDefault(),n.typeahead(n.data())})})}(window.jQuery) \ No newline at end of file -- cgit v1.2.3-54-g00ecf From d69e30cbf2cd76bdf87de138db030209ca43b2e1 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 24 Sep 2012 20:15:54 -0500 Subject: Add typeahead dropdown to front page packages search This uses the existing OpenSearch query endpoint to perform the search and displays the results accordingly. Signed-off-by: Dan McGee --- sitestatic/archweb.css | 25 +++++++++++++++++++++++++ templates/public/index.html | 18 ++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 70caf8fc..905a3ecb 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -486,6 +486,31 @@ h3 span.arrow { border: 1px solid #09c; } + .pkgsearch-typeahead { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + padding: 0.15em 0.1em; + margin: 0; + min-width: 10em; + font-size: 0.812em; + text-align: left; + list-style: none; + background-color: #f6f9fc; + border: 1px solid #09c; + } + + .pkgsearch-typeahead li a { + color: #000; + } + + .pkgsearch-typeahead li.active a { + color: #07b; + } + /* home: recent pkg updates */ #pkg-updates h3 { margin: 0 0 0.3em; diff --git a/templates/public/index.html b/templates/public/index.html index 60ae442d..4af5995e 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -215,4 +215,22 @@

      More Resources

    {% endcache %} + +{% load cdn %}{% jquery %} + + {% endblock %} -- cgit v1.2.3-54-g00ecf From f3e23371fa0473c82c28932e85570d94e5fc232a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 24 Sep 2012 20:21:15 -0500 Subject: Don't auto-select the first item in typeahead This assumption was baked into the Twitter bootstrap JS; kill it so it is still easy to do a freeform search if wanted. Signed-off-by: Dan McGee --- sitestatic/bootstrap-typeahead.js | 9 +++++---- sitestatic/bootstrap-typeahead.min.js | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/sitestatic/bootstrap-typeahead.js b/sitestatic/bootstrap-typeahead.js index c2ccdea2..3d355ae4 100644 --- a/sitestatic/bootstrap-typeahead.js +++ b/sitestatic/bootstrap-typeahead.js @@ -45,9 +45,11 @@ , select: function () { var val = this.$menu.find('.active').attr('data-value') - this.$element - .val(this.updater(val)) - .change() + if (val) { + this.$element + .val(this.updater(val)) + .change() + } return this.hide() } @@ -141,7 +143,6 @@ return i[0] }) - items.first().addClass('active') this.$menu.html(items) return this } diff --git a/sitestatic/bootstrap-typeahead.min.js b/sitestatic/bootstrap-typeahead.min.js index a5178904..7d555ed9 100644 --- a/sitestatic/bootstrap-typeahead.min.js +++ b/sitestatic/bootstrap-typeahead.min.js @@ -1 +1 @@ -!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.$menu=e(this.options.menu).appendTo("body"),this.source=this.options.source,this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.offset(),{height:this.$element[0].offsetHeight});return this.$menu.css({top:t.top+t.height,left:t.left}),this.$menu.show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length"+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),(e.browser.chrome||e.browser.webkit||e.browser.msie)&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this))},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=!~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},blur:function(e){var t=this;setTimeout(function(){t.hide()},150)},click:function(e){e.stopPropagation(),e.preventDefault(),this.select()},mouseenter:function(t){this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")}},e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},e.fn.typeahead.Constructor=t,e(function(){e("body").on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;t.preventDefault(),n.typeahead(n.data())})})}(window.jQuery) \ No newline at end of file +!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.$menu=e(this.options.menu).appendTo("body"),this.source=this.options.source,this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return e&&this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.offset(),{height:this.$element[0].offsetHeight});return this.$menu.css({top:t.top+t.height,left:t.left}),this.$menu.show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length"+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),(e.browser.chrome||e.browser.webkit||e.browser.msie)&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this))},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=!~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},blur:function(e){var t=this;setTimeout(function(){t.hide()},150)},click:function(e){e.stopPropagation(),e.preventDefault(),this.select()},mouseenter:function(t){this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")}},e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},e.fn.typeahead.Constructor=t,e(function(){e("body").on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;t.preventDefault(),n.typeahead(n.data())})})}(window.jQuery) \ No newline at end of file -- cgit v1.2.3-54-g00ecf From 8bf9147f47341d6efcad20931255888f7186bb7b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 24 Sep 2012 20:24:23 -0500 Subject: Update jQuery to 1.8.2 Signed-off-by: Dan McGee --- main/templatetags/cdn.py | 2 +- sitestatic/jquery-1.8.1.min.js | 2 -- sitestatic/jquery-1.8.2.min.js | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 sitestatic/jquery-1.8.1.min.js create mode 100644 sitestatic/jquery-1.8.2.min.js diff --git a/main/templatetags/cdn.py b/main/templatetags/cdn.py index a3ed465c..b1c65818 100644 --- a/main/templatetags/cdn.py +++ b/main/templatetags/cdn.py @@ -7,7 +7,7 @@ @register.simple_tag def jquery(): - version = '1.8.1' + version = '1.8.2' oncdn = getattr(settings, 'CDN_ENABLED', True) if oncdn: link = 'https://ajax.googleapis.com/ajax/libs/jquery/' \ diff --git a/sitestatic/jquery-1.8.1.min.js b/sitestatic/jquery-1.8.1.min.js deleted file mode 100644 index e7f2a292..00000000 --- a/sitestatic/jquery-1.8.1.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v@1.8.1 jquery.com | jquery.org/license */ -(function(a,b){function G(a){var b=F[a]={};return p.each(a.split(s),function(a,c){b[c]=!0}),b}function J(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(I,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:+d+""===d?+d:H.test(d)?p.parseJSON(d):d}catch(f){}p.data(a,c,d)}else d=b}return d}function K(a){var b;for(b in a){if(b==="data"&&p.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function ba(){return!1}function bb(){return!0}function bh(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function bi(a,b){do a=a[b];while(a&&a.nodeType!==1);return a}function bj(a,b,c){b=b||0;if(p.isFunction(b))return p.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return p.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=p.grep(a,function(a){return a.nodeType===1});if(be.test(b))return p.filter(b,d,!c);b=p.filter(b,d)}return p.grep(a,function(a,d){return p.inArray(a,b)>=0===c})}function bk(a){var b=bl.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function bC(a,b){return a.getElementsByTagName(b)[0]||a.appendChild(a.ownerDocument.createElement(b))}function bD(a,b){if(b.nodeType!==1||!p.hasData(a))return;var c,d,e,f=p._data(a),g=p._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;d").appendTo(e.body),c=b.css("display");b.remove();if(c==="none"||c===""){bI=e.body.appendChild(bI||p.extend(e.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!bJ||!bI.createElement)bJ=(bI.contentWindow||bI.contentDocument).document,bJ.write(""),bJ.close();b=bJ.body.appendChild(bJ.createElement(a)),c=bH(b,"display"),e.body.removeChild(bI)}return bS[a]=c,c}function ci(a,b,c,d){var e;if(p.isArray(b))p.each(b,function(b,e){c||ce.test(a)?d(a,e):ci(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&p.type(b)==="object")for(e in b)ci(a+"["+e+"]",b[e],c,d);else d(a,b)}function cz(a){return function(b,c){typeof b!="string"&&(c=b,b="*");var d,e,f,g=b.toLowerCase().split(s),h=0,i=g.length;if(p.isFunction(c))for(;h)[^>]*$|#([\w\-]*)$)/,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,y=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,z=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,A=/^-ms-/,B=/-([\da-z])/gi,C=function(a,b){return(b+"").toUpperCase()},D=function(){e.addEventListener?(e.removeEventListener("DOMContentLoaded",D,!1),p.ready()):e.readyState==="complete"&&(e.detachEvent("onreadystatechange",D),p.ready())},E={};p.fn=p.prototype={constructor:p,init:function(a,c,d){var f,g,h,i;if(!a)return this;if(a.nodeType)return this.context=this[0]=a,this.length=1,this;if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?f=[null,a,null]:f=u.exec(a);if(f&&(f[1]||!c)){if(f[1])return c=c instanceof p?c[0]:c,i=c&&c.nodeType?c.ownerDocument||c:e,a=p.parseHTML(f[1],i,!0),v.test(f[1])&&p.isPlainObject(c)&&this.attr.call(a,c,!0),p.merge(this,a);g=e.getElementById(f[2]);if(g&&g.parentNode){if(g.id!==f[2])return d.find(a);this.length=1,this[0]=g}return this.context=e,this.selector=a,this}return!c||c.jquery?(c||d).find(a):this.constructor(c).find(a)}return p.isFunction(a)?d.ready(a):(a.selector!==b&&(this.selector=a.selector,this.context=a.context),p.makeArray(a,this))},selector:"",jquery:"1.8.1",length:0,size:function(){return this.length},toArray:function(){return k.call(this)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=p.merge(this.constructor(),a);return d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")"),d},each:function(a,b){return p.each(this,a,b)},ready:function(a){return p.ready.promise().done(a),this},eq:function(a){return a=+a,a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(k.apply(this,arguments),"slice",k.call(arguments).join(","))},map:function(a){return this.pushStack(p.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:j,sort:[].sort,splice:[].splice},p.fn.init.prototype=p.fn,p.extend=p.fn.extend=function(){var a,c,d,e,f,g,h=arguments[0]||{},i=1,j=arguments.length,k=!1;typeof h=="boolean"&&(k=h,h=arguments[1]||{},i=2),typeof h!="object"&&!p.isFunction(h)&&(h={}),j===i&&(h=this,--i);for(;i0)return;d.resolveWith(e,[p]),p.fn.trigger&&p(e).trigger("ready").off("ready")},isFunction:function(a){return p.type(a)==="function"},isArray:Array.isArray||function(a){return p.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):E[m.call(a)]||"object"},isPlainObject:function(a){if(!a||p.type(a)!=="object"||a.nodeType||p.isWindow(a))return!1;try{if(a.constructor&&!n.call(a,"constructor")&&!n.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||n.call(a,d)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},error:function(a){throw new Error(a)},parseHTML:function(a,b,c){var d;return!a||typeof a!="string"?null:(typeof b=="boolean"&&(c=b,b=0),b=b||e,(d=v.exec(a))?[b.createElement(d[1])]:(d=p.buildFragment([a],b,c?null:[]),p.merge([],(d.cacheable?p.clone(d.fragment):d.fragment).childNodes)))},parseJSON:function(b){if(!b||typeof b!="string")return null;b=p.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(w.test(b.replace(y,"@").replace(z,"]").replace(x,"")))return(new Function("return "+b))();p.error("Invalid JSON: "+b)},parseXML:function(c){var d,e;if(!c||typeof c!="string")return null;try{a.DOMParser?(e=new DOMParser,d=e.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(f){d=b}return(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&p.error("Invalid XML: "+c),d},noop:function(){},globalEval:function(b){b&&r.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(A,"ms-").replace(B,C)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var e,f=0,g=a.length,h=g===b||p.isFunction(a);if(d){if(h){for(e in a)if(c.apply(a[e],d)===!1)break}else for(;f0&&a[0]&&a[i-1]||i===0||p.isArray(a));if(j)for(;h-1)i.splice(c,1),e&&(c<=g&&g--,c<=h&&h--)}),this},has:function(a){return p.inArray(a,i)>-1},empty:function(){return i=[],this},disable:function(){return i=j=c=b,this},disabled:function(){return!i},lock:function(){return j=b,c||l.disable(),this},locked:function(){return!j},fireWith:function(a,b){return b=b||[],b=[a,b.slice?b.slice():b],i&&(!d||j)&&(e?j.push(b):k(b)),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!d}};return l},p.extend({Deferred:function(a){var b=[["resolve","done",p.Callbacks("once memory"),"resolved"],["reject","fail",p.Callbacks("once memory"),"rejected"],["notify","progress",p.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return p.Deferred(function(c){p.each(b,function(b,d){var f=d[0],g=a[b];e[d[1]](p.isFunction(g)?function(){var a=g.apply(this,arguments);a&&p.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f+"With"](this===e?c:this,[a])}:c[f])}),a=null}).promise()},promise:function(a){return typeof a=="object"?p.extend(a,d):d}},e={};return d.pipe=d.then,p.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[a^1][2].disable,b[2][2].lock),e[f[0]]=g.fire,e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=k.call(arguments),d=c.length,e=d!==1||a&&p.isFunction(a.promise)?d:0,f=e===1?a:p.Deferred(),g=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?k.call(arguments):d,c===h?f.notifyWith(b,c):--e||f.resolveWith(b,c)}},h,i,j;if(d>1){h=new Array(d),i=new Array(d),j=new Array(d);for(;b

    Arch
    {{ pkg.arch.name }} {{ pkg.repo.name|capfirst }} {% pkg_details_link pkg %}
    a",c=n.getElementsByTagName("*"),d=n.getElementsByTagName("a")[0],d.style.cssText="top:1px;float:left;opacity:.5";if(!c||!c.length||!d)return{};f=e.createElement("select"),g=f.appendChild(e.createElement("option")),h=n.getElementsByTagName("input")[0],b={leadingWhitespace:n.firstChild.nodeType===3,tbody:!n.getElementsByTagName("tbody").length,htmlSerialize:!!n.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.5/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:h.value==="on",optSelected:g.selected,getSetAttribute:n.className!=="t",enctype:!!e.createElement("form").enctype,html5Clone:e.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",boxModel:e.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},h.checked=!0,b.noCloneChecked=h.cloneNode(!0).checked,f.disabled=!0,b.optDisabled=!g.disabled;try{delete n.test}catch(o){b.deleteExpando=!1}!n.addEventListener&&n.attachEvent&&n.fireEvent&&(n.attachEvent("onclick",m=function(){b.noCloneEvent=!1}),n.cloneNode(!0).fireEvent("onclick"),n.detachEvent("onclick",m)),h=e.createElement("input"),h.value="t",h.setAttribute("type","radio"),b.radioValue=h.value==="t",h.setAttribute("checked","checked"),h.setAttribute("name","t"),n.appendChild(h),i=e.createDocumentFragment(),i.appendChild(n.lastChild),b.checkClone=i.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=h.checked,i.removeChild(h),i.appendChild(n);if(n.attachEvent)for(k in{submit:!0,change:!0,focusin:!0})j="on"+k,l=j in n,l||(n.setAttribute(j,"return;"),l=typeof n[j]=="function"),b[k+"Bubbles"]=l;return p(function(){var c,d,f,g,h="padding:0;margin:0;border:0;display:block;overflow:hidden;",i=e.getElementsByTagName("body")[0];if(!i)return;c=e.createElement("div"),c.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",i.insertBefore(c,i.firstChild),d=e.createElement("div"),c.appendChild(d),d.innerHTML="
    t
    ",f=d.getElementsByTagName("td"),f[0].style.cssText="padding:0;margin:0;border:0;display:none",l=f[0].offsetHeight===0,f[0].style.display="",f[1].style.display="none",b.reliableHiddenOffsets=l&&f[0].offsetHeight===0,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",b.boxSizing=d.offsetWidth===4,b.doesNotIncludeMarginInBodyOffset=i.offsetTop!==1,a.getComputedStyle&&(b.pixelPosition=(a.getComputedStyle(d,null)||{}).top!=="1%",b.boxSizingReliable=(a.getComputedStyle(d,null)||{width:"4px"}).width==="4px",g=e.createElement("div"),g.style.cssText=d.style.cssText=h,g.style.marginRight=g.style.width="0",d.style.width="1px",d.appendChild(g),b.reliableMarginRight=!parseFloat((a.getComputedStyle(g,null)||{}).marginRight)),typeof d.style.zoom!="undefined"&&(d.innerHTML="",d.style.cssText=h+"width:1px;padding:1px;display:inline;zoom:1",b.inlineBlockNeedsLayout=d.offsetWidth===3,d.style.display="block",d.style.overflow="visible",d.innerHTML="
    ",d.firstChild.style.width="5px",b.shrinkWrapBlocks=d.offsetWidth!==3,c.style.zoom=1),i.removeChild(c),c=d=f=g=null}),i.removeChild(n),c=d=f=g=h=i=n=null,b}();var H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,I=/([A-Z])/g;p.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(p.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){return a=a.nodeType?p.cache[a[p.expando]]:a[p.expando],!!a&&!K(a)},data:function(a,c,d,e){if(!p.acceptData(a))return;var f,g,h=p.expando,i=typeof c=="string",j=a.nodeType,k=j?p.cache:a,l=j?a[h]:a[h]&&h;if((!l||!k[l]||!e&&!k[l].data)&&i&&d===b)return;l||(j?a[h]=l=p.deletedIds.pop()||++p.uuid:l=h),k[l]||(k[l]={},j||(k[l].toJSON=p.noop));if(typeof c=="object"||typeof c=="function")e?k[l]=p.extend(k[l],c):k[l].data=p.extend(k[l].data,c);return f=k[l],e||(f.data||(f.data={}),f=f.data),d!==b&&(f[p.camelCase(c)]=d),i?(g=f[c],g==null&&(g=f[p.camelCase(c)])):g=f,g},removeData:function(a,b,c){if(!p.acceptData(a))return;var d,e,f,g=a.nodeType,h=g?p.cache:a,i=g?a[p.expando]:p.expando;if(!h[i])return;if(b){d=c?h[i]:h[i].data;if(d){p.isArray(b)||(b in d?b=[b]:(b=p.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,f=b.length;e1,null,!1))},removeData:function(a){return this.each(function(){p.removeData(this,a)})}}),p.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=p._data(a,b),c&&(!d||p.isArray(c)?d=p._data(a,b,p.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=p.queue(a,b),d=c.length,e=c.shift(),f=p._queueHooks(a,b),g=function(){p.dequeue(a,b)};e==="inprogress"&&(e=c.shift(),d--),e&&(b==="fx"&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return p._data(a,c)||p._data(a,c,{empty:p.Callbacks("once memory").add(function(){p.removeData(a,b+"queue",!0),p.removeData(a,c,!0)})})}}),p.fn.extend({queue:function(a,c){var d=2;return typeof a!="string"&&(c=a,a="fx",d--),arguments.length1)},removeAttr:function(a){return this.each(function(){p.removeAttr(this,a)})},prop:function(a,b){return p.access(this,p.prop,a,b,arguments.length>1)},removeProp:function(a){return a=p.propFix[a]||a,this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,f,g,h;if(p.isFunction(a))return this.each(function(b){p(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(s);for(c=0,d=this.length;c-1)d=d.replace(" "+c[f]+" "," ");e.className=a?p.trim(d):""}}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";return p.isFunction(a)?this.each(function(c){p(this).toggleClass(a.call(this,c,this.className,b),b)}):this.each(function(){if(c==="string"){var e,f=0,g=p(this),h=b,i=a.split(s);while(e=i[f++])h=d?h:!g.hasClass(e),g[h?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&p._data(this,"__className__",this.className),this.className=this.className||a===!1?"":p._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c-1)return!0;return!1},val:function(a){var c,d,e,f=this[0];if(!arguments.length){if(f)return c=p.valHooks[f.type]||p.valHooks[f.nodeName.toLowerCase()],c&&"get"in c&&(d=c.get(f,"value"))!==b?d:(d=f.value,typeof d=="string"?d.replace(P,""):d==null?"":d);return}return e=p.isFunction(a),this.each(function(d){var f,g=p(this);if(this.nodeType!==1)return;e?f=a.call(this,d,g.val()):f=a,f==null?f="":typeof f=="number"?f+="":p.isArray(f)&&(f=p.map(f,function(a){return a==null?"":a+""})),c=p.valHooks[this.type]||p.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,f,"value")===b)this.value=f})}}),p.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,f=a.selectedIndex,g=[],h=a.options,i=a.type==="select-one";if(f<0)return null;c=i?f:0,d=i?f+1:h.length;for(;c=0}),c.length||(a.selectedIndex=-1),c}}},attrFn:{},attr:function(a,c,d,e){var f,g,h,i=a.nodeType;if(!a||i===3||i===8||i===2)return;if(e&&p.isFunction(p.fn[c]))return p(a)[c](d);if(typeof a.getAttribute=="undefined")return p.prop(a,c,d);h=i!==1||!p.isXMLDoc(a),h&&(c=c.toLowerCase(),g=p.attrHooks[c]||(T.test(c)?M:L));if(d!==b){if(d===null){p.removeAttr(a,c);return}return g&&"set"in g&&h&&(f=g.set(a,d,c))!==b?f:(a.setAttribute(c,""+d),d)}return g&&"get"in g&&h&&(f=g.get(a,c))!==null?f:(f=a.getAttribute(c),f===null?b:f)},removeAttr:function(a,b){var c,d,e,f,g=0;if(b&&a.nodeType===1){d=b.split(s);for(;g=0}})});var V=/^(?:textarea|input|select)$/i,W=/^([^\.]*|)(?:\.(.+)|)$/,X=/(?:^|\s)hover(\.\S+|)\b/,Y=/^key/,Z=/^(?:mouse|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=function(a){return p.event.special.hover?a:a.replace(X,"mouseenter$1 mouseleave$1")};p.event={add:function(a,c,d,e,f){var g,h,i,j,k,l,m,n,o,q,r;if(a.nodeType===3||a.nodeType===8||!c||!d||!(g=p._data(a)))return;d.handler&&(o=d,d=o.handler,f=o.selector),d.guid||(d.guid=p.guid++),i=g.events,i||(g.events=i={}),h=g.handle,h||(g.handle=h=function(a){return typeof p!="undefined"&&(!a||p.event.triggered!==a.type)?p.event.dispatch.apply(h.elem,arguments):b},h.elem=a),c=p.trim(_(c)).split(" ");for(j=0;j=0&&(s=s.slice(0,-1),i=!0),s.indexOf(".")>=0&&(t=s.split("."),s=t.shift(),t.sort());if((!f||p.event.customEvent[s])&&!p.event.global[s])return;c=typeof c=="object"?c[p.expando]?c:new p.Event(s,c):new p.Event(s),c.type=s,c.isTrigger=!0,c.exclusive=i,c.namespace=t.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+t.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,m=s.indexOf(":")<0?"on"+s:"";if(!f){h=p.cache;for(j in h)h[j].events&&h[j].events[s]&&p.event.trigger(c,d,h[j].handle.elem,!0);return}c.result=b,c.target||(c.target=f),d=d!=null?p.makeArray(d):[],d.unshift(c),n=p.event.special[s]||{};if(n.trigger&&n.trigger.apply(f,d)===!1)return;q=[[f,n.bindType||s]];if(!g&&!n.noBubble&&!p.isWindow(f)){r=n.delegateType||s,k=$.test(r+s)?f:f.parentNode;for(l=f;k;k=k.parentNode)q.push([k,r]),l=k;l===(f.ownerDocument||e)&&q.push([l.defaultView||l.parentWindow||a,r])}for(j=0;j=0),h[l]&&j.push(k);j.length&&t.push({elem:f,matches:j})}n.length>o&&t.push({elem:this,matches:n.slice(o)});for(d=0;d0?this.on(b,null,a,c):this.trigger(b)},Y.test(b)&&(p.event.fixHooks[b]=p.event.keyHooks),Z.test(b)&&(p.event.fixHooks[b]=p.event.mouseHooks)}),function(a,b){function $(a,b,c,d){c=c||[],b=b||q;var e,f,g,j,k=b.nodeType;if(k!==1&&k!==9)return[];if(!a||typeof a!="string")return c;g=h(b);if(!g&&!d)if(e=L.exec(a))if(j=e[1]){if(k===9){f=b.getElementById(j);if(!f||!f.parentNode)return c;if(f.id===j)return c.push(f),c}else if(b.ownerDocument&&(f=b.ownerDocument.getElementById(j))&&i(b,f)&&f.id===j)return c.push(f),c}else{if(e[2])return u.apply(c,t.call(b.getElementsByTagName(a),0)),c;if((j=e[3])&&X&&b.getElementsByClassName)return u.apply(c,t.call(b.getElementsByClassName(j),0)),c}return bk(a,b,c,d,g)}function _(a){return function(b){var c=b.nodeName.toLowerCase();return c==="input"&&b.type===a}}function ba(a){return function(b){var c=b.nodeName.toLowerCase();return(c==="input"||c==="button")&&b.type===a}}function bb(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}function bc(a,b,c,d){var e,g,h,i,j,k,l,m,n,p,r=!c&&b!==q,s=(r?"":"")+a.replace(H,"$1"),u=y[o][s];if(u)return d?0:t.call(u,0);j=a,k=[],m=0,n=f.preFilter,p=f.filter;while(j){if(!e||(g=I.exec(j)))g&&(j=j.slice(g[0].length),h.selector=l),k.push(h=[]),l="",r&&(j=" "+j);e=!1;if(g=J.exec(j))l+=g[0],j=j.slice(g[0].length),e=h.push({part:g.pop().replace(H," "),string:g[0],captures:g});for(i in p)(g=S[i].exec(j))&&(!n[i]||(g=n[i](g,b,c)))&&(l+=g[0],j=j.slice(g[0].length),e=h.push({part:i,string:g.shift(),captures:g}));if(!e)break}return l&&(h.selector=l),d?j.length:j?$.error(a):t.call(y(s,k),0)}function bd(a,b,e,f){var g=b.dir,h=s++;return a||(a=function(a){return a===e}),b.first?function(b){while(b=b[g])if(b.nodeType===1)return a(b)&&b}:f?function(b){while(b=b[g])if(b.nodeType===1&&a(b))return b}:function(b){var e,f=h+"."+c,i=f+"."+d;while(b=b[g])if(b.nodeType===1){if((e=b[o])===i)return b.sizset;if(typeof e=="string"&&e.indexOf(f)===0){if(b.sizset)return b}else{b[o]=i;if(a(b))return b.sizset=!0,b;b.sizset=!1}}}}function be(a,b){return a?function(c){var d=b(c);return d&&a(d===!0?c:d)}:b}function bf(a,b,c){var d,e,g=0;for(;d=a[g];g++)f.relative[d.part]?e=bd(e,f.relative[d.part],b,c):e=be(e,f.filter[d.part].apply(null,d.captures.concat(b,c)));return e}function bg(a){return function(b){var c,d=0;for(;c=a[d];d++)if(c(b))return!0;return!1}}function bh(a,b,c,d){var e=0,f=b.length;for(;e0?i(h,c,g):[]}function bj(a,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q,r,s=0,t=a.length,v=S.POS,w=new RegExp("^"+v.source+"(?!"+A+")","i"),x=function(){var a=1,c=arguments.length-2;for(;al){g+=k.slice(l,n.index),l=p,q=[c],J.test(g)&&(m&&(q=m),m=e);if(r=O.test(g))g=g.slice(0,-5).replace(J,"$&*"),l++;n.length>1&&n[0].replace(w,x),m=bi(g,n[1],n[2],q,m,r)}g=""}}o||(g+=k),o=!1}g?J.test(g)?bh(g,m||[c],d,e):$(g,c,d,e?e.concat(m):m):u.apply(d,m)}return t===1?d:$.uniqueSort(d)}function bk(a,b,e,g,h){a=a.replace(H,"$1");var i,k,l,m,n,o,p,q,r,s,v=bc(a,b,h),w=b.nodeType;if(S.POS.test(a))return bj(v,b,e,g);if(g)i=t.call(g,0);else if(v.length===1){if((o=t.call(v[0],0)).length>2&&(p=o[0]).part==="ID"&&w===9&&!h&&f.relative[o[1].part]){b=f.find.ID(p.captures[0].replace(R,""),b,h)[0];if(!b)return e;a=a.slice(o.shift().string.length)}r=(v=N.exec(o[0].string))&&!v.index&&b.parentNode||b,q="";for(n=o.length-1;n>=0;n--){p=o[n],s=p.part,q=p.string+q;if(f.relative[s])break;if(f.order.test(s)){i=f.find[s](p.captures[0].replace(R,""),r,h);if(i==null)continue;a=a.slice(0,a.length-q.length)+q.replace(S[s],""),a||u.apply(e,t.call(i,0));break}}}if(a){k=j(a,b,h),c=k.dirruns++,i==null&&(i=f.find.TAG("*",N.test(a)&&b.parentNode||b));for(n=0;m=i[n];n++)d=k.runs++,k(m)&&e.push(m)}return e}var c,d,e,f,g,h,i,j,k,l,m=!0,n="undefined",o=("sizcache"+Math.random()).replace(".",""),q=a.document,r=q.documentElement,s=0,t=[].slice,u=[].push,v=function(a,b){return a[o]=b||!0,a},w=function(){var a={},b=[];return v(function(c,d){return b.push(c)>f.cacheLength&&delete a[b.shift()],a[c]=d},a)},x=w(),y=w(),z=w(),A="[\\x20\\t\\r\\n\\f]",B="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",C=B.replace("w","w#"),D="([*^$|!~]?=)",E="\\["+A+"*("+B+")"+A+"*(?:"+D+A+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+C+")|)|)"+A+"*\\]",F=":("+B+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+E+")|[^:]|\\\\.)*|.*))\\)|)",G=":(nth|eq|gt|lt|first|last|even|odd)(?:\\(((?:-\\d)?\\d*)\\)|)(?=[^-]|$)",H=new RegExp("^"+A+"+|((?:^|[^\\\\])(?:\\\\.)*)"+A+"+$","g"),I=new RegExp("^"+A+"*,"+A+"*"),J=new RegExp("^"+A+"*([\\x20\\t\\r\\n\\f>+~])"+A+"*"),K=new RegExp(F),L=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,M=/^:not/,N=/[\x20\t\r\n\f]*[+~]/,O=/:not\($/,P=/h\d/i,Q=/input|select|textarea|button/i,R=/\\(?!\\)/g,S={ID:new RegExp("^#("+B+")"),CLASS:new RegExp("^\\.("+B+")"),NAME:new RegExp("^\\[name=['\"]?("+B+")['\"]?\\]"),TAG:new RegExp("^("+B.replace("w","w*")+")"),ATTR:new RegExp("^"+E),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|nth|last|first)-child(?:\\("+A+"*(even|odd|(([+-]|)(\\d*)n|)"+A+"*(?:([+-]|)"+A+"*(\\d+)|))"+A+"*\\)|)","i"),POS:new RegExp(G,"ig"),needsContext:new RegExp("^"+A+"*[>+~]|"+G,"i")},T=function(a){var b=q.createElement("div");try{return a(b)}catch(c){return!1}finally{b=null}},U=T(function(a){return a.appendChild(q.createComment("")),!a.getElementsByTagName("*").length}),V=T(function(a){return a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!==n&&a.firstChild.getAttribute("href")==="#"}),W=T(function(a){a.innerHTML="";var b=typeof a.lastChild.getAttribute("multiple");return b!=="boolean"&&b!=="string"}),X=T(function(a){return a.innerHTML="",!a.getElementsByClassName||!a.getElementsByClassName("e").length?!1:(a.lastChild.className="e",a.getElementsByClassName("e").length===2)}),Y=T(function(a){a.id=o+0,a.innerHTML="
    ",r.insertBefore(a,r.firstChild);var b=q.getElementsByName&&q.getElementsByName(o).length===2+q.getElementsByName(o+0).length;return e=!q.getElementById(o),r.removeChild(a),b});try{t.call(r.childNodes,0)[0].nodeType}catch(Z){t=function(a){var b,c=[];for(;b=this[a];a++)c.push(b);return c}}$.matches=function(a,b){return $(a,null,null,b)},$.matchesSelector=function(a,b){return $(b,null,null,[a]).length>0},g=$.getText=function(a){var b,c="",d=0,e=a.nodeType;if(e){if(e===1||e===9||e===11){if(typeof a.textContent=="string")return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=g(a)}else if(e===3||e===4)return a.nodeValue}else for(;b=a[d];d++)c+=g(b);return c},h=$.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?b.nodeName!=="HTML":!1},i=$.contains=r.contains?function(a,b){var c=a.nodeType===9?a.documentElement:a,d=b&&b.parentNode;return a===d||!!(d&&d.nodeType===1&&c.contains&&c.contains(d))}:r.compareDocumentPosition?function(a,b){return b&&!!(a.compareDocumentPosition(b)&16)}:function(a,b){while(b=b.parentNode)if(b===a)return!0;return!1},$.attr=function(a,b){var c,d=h(a);return d||(b=b.toLowerCase()),f.attrHandle[b]?f.attrHandle[b](a):W||d?a.getAttribute(b):(c=a.getAttributeNode(b),c?typeof a[b]=="boolean"?a[b]?b:null:c.specified?c.value:null:null)},f=$.selectors={cacheLength:50,createPseudo:v,match:S,order:new RegExp("ID|TAG"+(Y?"|NAME":"")+(X?"|CLASS":"")),attrHandle:V?{}:{href:function(a){return a.getAttribute("href",2)},type:function(a){return a.getAttribute("type")}},find:{ID:e?function(a,b,c){if(typeof b.getElementById!==n&&!c){var d=b.getElementById(a);return d&&d.parentNode?[d]:[]}}:function(a,c,d){if(typeof c.getElementById!==n&&!d){var e=c.getElementById(a);return e?e.id===a||typeof e.getAttributeNode!==n&&e.getAttributeNode("id").value===a?[e]:b:[]}},TAG:U?function(a,b){if(typeof b.getElementsByTagName!==n)return b.getElementsByTagName(a)}:function(a,b){var c=b.getElementsByTagName(a);if(a==="*"){var d,e=[],f=0;for(;d=c[f];f++)d.nodeType===1&&e.push(d);return e}return c},NAME:function(a,b){if(typeof b.getElementsByName!==n)return b.getElementsByName(name)},CLASS:function(a,b,c){if(typeof b.getElementsByClassName!==n&&!c)return b.getElementsByClassName(a)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(R,""),a[3]=(a[4]||a[5]||"").replace(R,""),a[2]==="~="&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),a[1]==="nth"?(a[2]||$.error(a[0]),a[3]=+(a[3]?a[4]+(a[5]||1):2*(a[2]==="even"||a[2]==="odd")),a[4]=+(a[6]+a[7]||a[2]==="odd")):a[2]&&$.error(a[0]),a},PSEUDO:function(a,b,c){var d,e;if(S.CHILD.test(a[0]))return null;if(a[3])a[2]=a[3];else if(d=a[4])K.test(d)&&(e=bc(d,b,c,!0))&&(e=d.indexOf(")",d.length-e)-d.length)&&(d=d.slice(0,e),a[0]=a[0].slice(0,e)),a[2]=d;return a.slice(0,3)}},filter:{ID:e?function(a){return a=a.replace(R,""),function(b){return b.getAttribute("id")===a}}:function(a){return a=a.replace(R,""),function(b){var c=typeof b.getAttributeNode!==n&&b.getAttributeNode("id");return c&&c.value===a}},TAG:function(a){return a==="*"?function(){return!0}:(a=a.replace(R,"").toLowerCase(),function(b){return b.nodeName&&b.nodeName.toLowerCase()===a})},CLASS:function(a){var b=x[o][a];return b||(b=x(a,new RegExp("(^|"+A+")"+a+"("+A+"|$)"))),function(a){return b.test(a.className||typeof a.getAttribute!==n&&a.getAttribute("class")||"")}},ATTR:function(a,b,c){return b?function(d){var e=$.attr(d,a),f=e+"";if(e==null)return b==="!=";switch(b){case"=":return f===c;case"!=":return f!==c;case"^=":return c&&f.indexOf(c)===0;case"*=":return c&&f.indexOf(c)>-1;case"$=":return c&&f.substr(f.length-c.length)===c;case"~=":return(" "+f+" ").indexOf(c)>-1;case"|=":return f===c||f.substr(0,c.length+1)===c+"-"}}:function(b){return $.attr(b,a)!=null}},CHILD:function(a,b,c,d){if(a==="nth"){var e=s++;return function(a){var b,f,g=0,h=a;if(c===1&&d===0)return!0;b=a.parentNode;if(b&&(b[o]!==e||!a.sizset)){for(h=b.firstChild;h;h=h.nextSibling)if(h.nodeType===1){h.sizset=++g;if(h===a)break}b[o]=e}return f=a.sizset-d,c===0?f===0:f%c===0&&f/c>=0}}return function(b){var c=b;switch(a){case"only":case"first":while(c=c.previousSibling)if(c.nodeType===1)return!1;if(a==="first")return!0;c=b;case"last":while(c=c.nextSibling)if(c.nodeType===1)return!1;return!0}}},PSEUDO:function(a,b,c,d){var e,g=f.pseudos[a]||f.pseudos[a.toLowerCase()];return g||$.error("unsupported pseudo: "+a),g[o]?g(b,c,d):g.length>1?(e=[a,a,"",b],function(a){return g(a,0,e)}):g}},pseudos:{not:v(function(a,b,c){var d=j(a.replace(H,"$1"),b,c);return function(a){return!d(a)}}),enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&!!a.checked||b==="option"&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},parent:function(a){return!f.pseudos.empty(a)},empty:function(a){var b;a=a.firstChild;while(a){if(a.nodeName>"@"||(b=a.nodeType)===3||b===4)return!1;a=a.nextSibling}return!0},contains:v(function(a){return function(b){return(b.textContent||b.innerText||g(b)).indexOf(a)>-1}}),has:v(function(a){return function(b){return $(a,b).length>0}}),header:function(a){return P.test(a.nodeName)},text:function(a){var b,c;return a.nodeName.toLowerCase()==="input"&&(b=a.type)==="text"&&((c=a.getAttribute("type"))==null||c.toLowerCase()===b)},radio:_("radio"),checkbox:_("checkbox"),file:_("file"),password:_("password"),image:_("image"),submit:ba("submit"),reset:ba("reset"),button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&a.type==="button"||b==="button"},input:function(a){return Q.test(a.nodeName)},focus:function(a){var b=a.ownerDocument;return a===b.activeElement&&(!b.hasFocus||b.hasFocus())&&(!!a.type||!!a.href)},active:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b,c){return c?a.slice(1):[a[0]]},last:function(a,b,c){var d=a.pop();return c?a:[d]},even:function(a,b,c){var d=[],e=c?1:0,f=a.length;for(;e",a.querySelectorAll("[selected]").length||e.push("\\["+A+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),a.querySelectorAll(":checked").length||e.push(":checked")}),T(function(a){a.innerHTML="

    ",a.querySelectorAll("[test^='']").length&&e.push("[*^$]="+A+"*(?:\"\"|'')"),a.innerHTML="",a.querySelectorAll(":enabled").length||e.push(":enabled",":disabled")}),e=e.length&&new RegExp(e.join("|")),bk=function(a,d,f,g,h){if(!g&&!h&&(!e||!e.test(a)))if(d.nodeType===9)try{return u.apply(f,t.call(d.querySelectorAll(a),0)),f}catch(i){}else if(d.nodeType===1&&d.nodeName.toLowerCase()!=="object"){var j,k,l,m=d.getAttribute("id"),n=m||o,p=N.test(a)&&d.parentNode||d;m?n=n.replace(c,"\\$&"):d.setAttribute("id",n),j=bc(a,d,h),n="[id='"+n+"']";for(k=0,l=j.length;k0})}(),f.setFilters.nth=f.setFilters.eq,f.filters=f.pseudos,$.attr=p.attr,p.find=$,p.expr=$.selectors,p.expr[":"]=p.expr.pseudos,p.unique=$.uniqueSort,p.text=$.getText,p.isXMLDoc=$.isXML,p.contains=$.contains}(a);var bc=/Until$/,bd=/^(?:parents|prev(?:Until|All))/,be=/^.[^:#\[\.,]*$/,bf=p.expr.match.needsContext,bg={children:!0,contents:!0,next:!0,prev:!0};p.fn.extend({find:function(a){var b,c,d,e,f,g,h=this;if(typeof a!="string")return p(a).filter(function(){for(b=0,c=h.length;b0)for(e=d;e=0:p.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c,d=0,e=this.length,f=[],g=bf.test(a)||typeof a!="string"?p(a,b||this.context):0;for(;d-1:p.find.matchesSelector(c,a)){f.push(c);break}c=c.parentNode}}return f=f.length>1?p.unique(f):f,this.pushStack(f,"closest",a)},index:function(a){return a?typeof a=="string"?p.inArray(this[0],p(a)):p.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(a,b){var c=typeof a=="string"?p(a,b):p.makeArray(a&&a.nodeType?[a]:a),d=p.merge(this.get(),c);return this.pushStack(bh(c[0])||bh(d[0])?d:p.unique(d))},addBack:function(a){return this.add(a==null?this.prevObject:this.prevObject.filter(a))}}),p.fn.andSelf=p.fn.addBack,p.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return p.dir(a,"parentNode")},parentsUntil:function(a,b,c){return p.dir(a,"parentNode",c)},next:function(a){return bi(a,"nextSibling")},prev:function(a){return bi(a,"previousSibling")},nextAll:function(a){return p.dir(a,"nextSibling")},prevAll:function(a){return p.dir(a,"previousSibling")},nextUntil:function(a,b,c){return p.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return p.dir(a,"previousSibling",c)},siblings:function(a){return p.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return p.sibling(a.firstChild)},contents:function(a){return p.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:p.merge([],a.childNodes)}},function(a,b){p.fn[a]=function(c,d){var e=p.map(this,b,c);return bc.test(a)||(d=c),d&&typeof d=="string"&&(e=p.filter(d,e)),e=this.length>1&&!bg[a]?p.unique(e):e,this.length>1&&bd.test(a)&&(e=e.reverse()),this.pushStack(e,a,k.call(arguments).join(","))}}),p.extend({filter:function(a,b,c){return c&&(a=":not("+a+")"),b.length===1?p.find.matchesSelector(b[0],a)?[b[0]]:[]:p.find.matches(a,b)},dir:function(a,c,d){var e=[],f=a[c];while(f&&f.nodeType!==9&&(d===b||f.nodeType!==1||!p(f).is(d)))f.nodeType===1&&e.push(f),f=f[c];return e},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var bl="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",bm=/ jQuery\d+="(?:null|\d+)"/g,bn=/^\s+/,bo=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bp=/<([\w:]+)/,bq=/]","i"),bv=/^(?:checkbox|radio)$/,bw=/checked\s*(?:[^=]|=\s*.checked.)/i,bx=/\/(java|ecma)script/i,by=/^\s*\s*$/g,bz={option:[1,""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]},bA=bk(e),bB=bA.appendChild(e.createElement("div"));bz.optgroup=bz.option,bz.tbody=bz.tfoot=bz.colgroup=bz.caption=bz.thead,bz.th=bz.td,p.support.htmlSerialize||(bz._default=[1,"X
    ","
    "]),p.fn.extend({text:function(a){return p.access(this,function(a){return a===b?p.text(this):this.empty().append((this[0]&&this[0].ownerDocument||e).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(p.isFunction(a))return this.each(function(b){p(this).wrapAll(a.call(this,b))});if(this[0]){var b=p(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return p.isFunction(a)?this.each(function(b){p(this).wrapInner(a.call(this,b))}):this.each(function(){var b=p(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=p.isFunction(a);return this.each(function(c){p(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){p.nodeName(this,"body")||p(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(a,this.firstChild)})},before:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(a,this),"before",this.selector)}},after:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(this,a),"after",this.selector)}},remove:function(a,b){var c,d=0;for(;(c=this[d])!=null;d++)if(!a||p.filter(a,[c]).length)!b&&c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),p.cleanData([c])),c.parentNode&&c.parentNode.removeChild(c);return this},empty:function(){var a,b=0;for(;(a=this[b])!=null;b++){a.nodeType===1&&p.cleanData(a.getElementsByTagName("*"));while(a.firstChild)a.removeChild(a.firstChild)}return this},clone:function(a,b){return a=a==null?!1:a,b=b==null?a:b,this.map(function(){return p.clone(this,a,b)})},html:function(a){return p.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(bm,""):b;if(typeof a=="string"&&!bs.test(a)&&(p.support.htmlSerialize||!bu.test(a))&&(p.support.leadingWhitespace||!bn.test(a))&&!bz[(bp.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(bo,"<$1>");try{for(;d1&&typeof j=="string"&&bw.test(j))return this.each(function(){p(this).domManip(a,c,d)});if(p.isFunction(j))return this.each(function(e){var f=p(this);a[0]=j.call(this,e,c?f.html():b),f.domManip(a,c,d)});if(this[0]){e=p.buildFragment(a,this,k),g=e.fragment,f=g.firstChild,g.childNodes.length===1&&(g=f);if(f){c=c&&p.nodeName(f,"tr");for(h=e.cacheable||l-1;i0?this.clone(!0):this).get(),p(g[e])[b](d),f=f.concat(d);return this.pushStack(f,a,g.selector)}}),p.extend({clone:function(a,b,c){var d,e,f,g;p.support.html5Clone||p.isXMLDoc(a)||!bu.test("<"+a.nodeName+">")?g=a.cloneNode(!0):(bB.innerHTML=a.outerHTML,bB.removeChild(g=bB.firstChild));if((!p.support.noCloneEvent||!p.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!p.isXMLDoc(a)){bE(a,g),d=bF(a),e=bF(g);for(f=0;d[f];++f)e[f]&&bE(d[f],e[f])}if(b){bD(a,g);if(c){d=bF(a),e=bF(g);for(f=0;d[f];++f)bD(d[f],e[f])}}return d=e=null,g},clean:function(a,b,c,d){var f,g,h,i,j,k,l,m,n,o,q,r,s=b===e&&bA,t=[];if(!b||typeof b.createDocumentFragment=="undefined")b=e;for(f=0;(h=a[f])!=null;f++){typeof h=="number"&&(h+="");if(!h)continue;if(typeof h=="string")if(!br.test(h))h=b.createTextNode(h);else{s=s||bk(b),l=b.createElement("div"),s.appendChild(l),h=h.replace(bo,"<$1>"),i=(bp.exec(h)||["",""])[1].toLowerCase(),j=bz[i]||bz._default,k=j[0],l.innerHTML=j[1]+h+j[2];while(k--)l=l.lastChild;if(!p.support.tbody){m=bq.test(h),n=i==="table"&&!m?l.firstChild&&l.firstChild.childNodes:j[1]===""&&!m?l.childNodes:[];for(g=n.length-1;g>=0;--g)p.nodeName(n[g],"tbody")&&!n[g].childNodes.length&&n[g].parentNode.removeChild(n[g])}!p.support.leadingWhitespace&&bn.test(h)&&l.insertBefore(b.createTextNode(bn.exec(h)[0]),l.firstChild),h=l.childNodes,l.parentNode.removeChild(l)}h.nodeType?t.push(h):p.merge(t,h)}l&&(h=l=s=null);if(!p.support.appendChecked)for(f=0;(h=t[f])!=null;f++)p.nodeName(h,"input")?bG(h):typeof h.getElementsByTagName!="undefined"&&p.grep(h.getElementsByTagName("input"),bG);if(c){q=function(a){if(!a.type||bx.test(a.type))return d?d.push(a.parentNode?a.parentNode.removeChild(a):a):c.appendChild(a)};for(f=0;(h=t[f])!=null;f++)if(!p.nodeName(h,"script")||!q(h))c.appendChild(h),typeof h.getElementsByTagName!="undefined"&&(r=p.grep(p.merge([],h.getElementsByTagName("script")),q),t.splice.apply(t,[f+1,0].concat(r)),f+=r.length)}return t},cleanData:function(a,b){var c,d,e,f,g=0,h=p.expando,i=p.cache,j=p.support.deleteExpando,k=p.event.special;for(;(e=a[g])!=null;g++)if(b||p.acceptData(e)){d=e[h],c=d&&i[d];if(c){if(c.events)for(f in c.events)k[f]?p.event.remove(e,f):p.removeEvent(e,f,c.handle);i[d]&&(delete i[d],j?delete e[h]:e.removeAttribute?e.removeAttribute(h):e[h]=null,p.deletedIds.push(d))}}}}),function(){var a,b;p.uaMatch=function(a){a=a.toLowerCase();var b=/(chrome)[ \/]([\w.]+)/.exec(a)||/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||a.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},a=p.uaMatch(g.userAgent),b={},a.browser&&(b[a.browser]=!0,b.version=a.version),b.chrome?b.webkit=!0:b.webkit&&(b.safari=!0),p.browser=b,p.sub=function(){function a(b,c){return new a.fn.init(b,c)}p.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function c(c,d){return d&&d instanceof p&&!(d instanceof a)&&(d=a(d)),p.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(e);return a}}();var bH,bI,bJ,bK=/alpha\([^)]*\)/i,bL=/opacity=([^)]*)/,bM=/^(top|right|bottom|left)$/,bN=/^(none|table(?!-c[ea]).+)/,bO=/^margin/,bP=new RegExp("^("+q+")(.*)$","i"),bQ=new RegExp("^("+q+")(?!px)[a-z%]+$","i"),bR=new RegExp("^([-+])=("+q+")","i"),bS={},bT={position:"absolute",visibility:"hidden",display:"block"},bU={letterSpacing:0,fontWeight:400},bV=["Top","Right","Bottom","Left"],bW=["Webkit","O","Moz","ms"],bX=p.fn.toggle;p.fn.extend({css:function(a,c){return p.access(this,function(a,c,d){return d!==b?p.style(a,c,d):p.css(a,c)},a,c,arguments.length>1)},show:function(){return b$(this,!0)},hide:function(){return b$(this)},toggle:function(a,b){var c=typeof a=="boolean";return p.isFunction(a)&&p.isFunction(b)?bX.apply(this,arguments):this.each(function(){(c?a:bZ(this))?p(this).show():p(this).hide()})}}),p.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bH(a,"opacity");return c===""?"1":c}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":p.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!a||a.nodeType===3||a.nodeType===8||!a.style)return;var f,g,h,i=p.camelCase(c),j=a.style;c=p.cssProps[i]||(p.cssProps[i]=bY(j,i)),h=p.cssHooks[c]||p.cssHooks[i];if(d===b)return h&&"get"in h&&(f=h.get(a,!1,e))!==b?f:j[c];g=typeof d,g==="string"&&(f=bR.exec(d))&&(d=(f[1]+1)*f[2]+parseFloat(p.css(a,c)),g="number");if(d==null||g==="number"&&isNaN(d))return;g==="number"&&!p.cssNumber[i]&&(d+="px");if(!h||!("set"in h)||(d=h.set(a,d,e))!==b)try{j[c]=d}catch(k){}},css:function(a,c,d,e){var f,g,h,i=p.camelCase(c);return c=p.cssProps[i]||(p.cssProps[i]=bY(a.style,i)),h=p.cssHooks[c]||p.cssHooks[i],h&&"get"in h&&(f=h.get(a,!0,e)),f===b&&(f=bH(a,c)),f==="normal"&&c in bU&&(f=bU[c]),d||e!==b?(g=parseFloat(f),d||p.isNumeric(g)?g||0:f):f},swap:function(a,b,c){var d,e,f={};for(e in b)f[e]=a.style[e],a.style[e]=b[e];d=c.call(a);for(e in b)a.style[e]=f[e];return d}}),a.getComputedStyle?bH=function(b,c){var d,e,f,g,h=a.getComputedStyle(b,null),i=b.style;return h&&(d=h[c],d===""&&!p.contains(b.ownerDocument,b)&&(d=p.style(b,c)),bQ.test(d)&&bO.test(c)&&(e=i.width,f=i.minWidth,g=i.maxWidth,i.minWidth=i.maxWidth=i.width=d,d=h.width,i.width=e,i.minWidth=f,i.maxWidth=g)),d}:e.documentElement.currentStyle&&(bH=function(a,b){var c,d,e=a.currentStyle&&a.currentStyle[b],f=a.style;return e==null&&f&&f[b]&&(e=f[b]),bQ.test(e)&&!bM.test(b)&&(c=f.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":e,e=f.pixelLeft+"px",f.left=c,d&&(a.runtimeStyle.left=d)),e===""?"auto":e}),p.each(["height","width"],function(a,b){p.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth===0&&bN.test(bH(a,"display"))?p.swap(a,bT,function(){return cb(a,b,d)}):cb(a,b,d)},set:function(a,c,d){return b_(a,c,d?ca(a,b,d,p.support.boxSizing&&p.css(a,"boxSizing")==="border-box"):0)}}}),p.support.opacity||(p.cssHooks.opacity={get:function(a,b){return bL.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=p.isNumeric(b)?"alpha(opacity="+b*100+")":"",f=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&p.trim(f.replace(bK,""))===""&&c.removeAttribute){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bK.test(f)?f.replace(bK,e):f+" "+e}}),p(function(){p.support.reliableMarginRight||(p.cssHooks.marginRight={get:function(a,b){return p.swap(a,{display:"inline-block"},function(){if(b)return bH(a,"marginRight")})}}),!p.support.pixelPosition&&p.fn.position&&p.each(["top","left"],function(a,b){p.cssHooks[b]={get:function(a,c){if(c){var d=bH(a,b);return bQ.test(d)?p(a).position()[b]+"px":d}}}})}),p.expr&&p.expr.filters&&(p.expr.filters.hidden=function(a){return a.offsetWidth===0&&a.offsetHeight===0||!p.support.reliableHiddenOffsets&&(a.style&&a.style.display||bH(a,"display"))==="none"},p.expr.filters.visible=function(a){return!p.expr.filters.hidden(a)}),p.each({margin:"",padding:"",border:"Width"},function(a,b){p.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bV[d]+b]=e[d]||e[d-2]||e[0];return f}},bO.test(a)||(p.cssHooks[a+b].set=b_)});var cd=/%20/g,ce=/\[\]$/,cf=/\r?\n/g,cg=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ch=/^(?:select|textarea)/i;p.fn.extend({serialize:function(){return p.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?p.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ch.test(this.nodeName)||cg.test(this.type))}).map(function(a,b){var c=p(this).val();return c==null?null:p.isArray(c)?p.map(c,function(a,c){return{name:b.name,value:a.replace(cf,"\r\n")}}):{name:b.name,value:c.replace(cf,"\r\n")}}).get()}}),p.param=function(a,c){var d,e=[],f=function(a,b){b=p.isFunction(b)?b():b==null?"":b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=p.ajaxSettings&&p.ajaxSettings.traditional);if(p.isArray(a)||a.jquery&&!p.isPlainObject(a))p.each(a,function(){f(this.name,this.value)});else for(d in a)ci(d,a[d],c,f);return e.join("&").replace(cd,"+")};var cj,ck,cl=/#.*$/,cm=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,cn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,co=/^(?:GET|HEAD)$/,cp=/^\/\//,cq=/\?/,cr=/)<[^<]*)*<\/script>/gi,cs=/([?&])_=[^&]*/,ct=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,cu=p.fn.load,cv={},cw={},cx=["*/"]+["*"];try{cj=f.href}catch(cy){cj=e.createElement("a"),cj.href="",cj=cj.href}ck=ct.exec(cj.toLowerCase())||[],p.fn.load=function(a,c,d){if(typeof a!="string"&&cu)return cu.apply(this,arguments);if(!this.length)return this;var e,f,g,h=this,i=a.indexOf(" ");return i>=0&&(e=a.slice(i,a.length),a=a.slice(0,i)),p.isFunction(c)?(d=c,c=b):c&&typeof c=="object"&&(f="POST"),p.ajax({url:a,type:f,dataType:"html",data:c,complete:function(a,b){d&&h.each(d,g||[a.responseText,b,a])}}).done(function(a){g=arguments,h.html(e?p("
    ").append(a.replace(cr,"")).find(e):a)}),this},p.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){p.fn[b]=function(a){return this.on(b,a)}}),p.each(["get","post"],function(a,c){p[c]=function(a,d,e,f){return p.isFunction(d)&&(f=f||e,e=d,d=b),p.ajax({type:c,url:a,data:d,success:e,dataType:f})}}),p.extend({getScript:function(a,c){return p.get(a,b,c,"script")},getJSON:function(a,b,c){return p.get(a,b,c,"json")},ajaxSetup:function(a,b){return b?cB(a,p.ajaxSettings):(b=a,a=p.ajaxSettings),cB(a,b),a},ajaxSettings:{url:cj,isLocal:cn.test(ck[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":cx},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":p.parseJSON,"text xml":p.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:cz(cv),ajaxTransport:cz(cw),ajax:function(a,c){function y(a,c,f,i){var k,s,t,u,w,y=c;if(v===2)return;v=2,h&&clearTimeout(h),g=b,e=i||"",x.readyState=a>0?4:0,f&&(u=cC(l,x,f));if(a>=200&&a<300||a===304)l.ifModified&&(w=x.getResponseHeader("Last-Modified"),w&&(p.lastModified[d]=w),w=x.getResponseHeader("Etag"),w&&(p.etag[d]=w)),a===304?(y="notmodified",k=!0):(k=cD(l,u),y=k.state,s=k.data,t=k.error,k=!t);else{t=y;if(!y||a)y="error",a<0&&(a=0)}x.status=a,x.statusText=""+(c||y),k?o.resolveWith(m,[s,y,x]):o.rejectWith(m,[x,y,t]),x.statusCode(r),r=b,j&&n.trigger("ajax"+(k?"Success":"Error"),[x,l,k?s:t]),q.fireWith(m,[x,y]),j&&(n.trigger("ajaxComplete",[x,l]),--p.active||p.event.trigger("ajaxStop"))}typeof a=="object"&&(c=a,a=b),c=c||{};var d,e,f,g,h,i,j,k,l=p.ajaxSetup({},c),m=l.context||l,n=m!==l&&(m.nodeType||m instanceof p)?p(m):p.event,o=p.Deferred(),q=p.Callbacks("once memory"),r=l.statusCode||{},t={},u={},v=0,w="canceled",x={readyState:0,setRequestHeader:function(a,b){if(!v){var c=a.toLowerCase();a=u[c]=u[c]||a,t[a]=b}return this},getAllResponseHeaders:function(){return v===2?e:null},getResponseHeader:function(a){var c;if(v===2){if(!f){f={};while(c=cm.exec(e))f[c[1].toLowerCase()]=c[2]}c=f[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){return v||(l.mimeType=a),this},abort:function(a){return a=a||w,g&&g.abort(a),y(0,a),this}};o.promise(x),x.success=x.done,x.error=x.fail,x.complete=q.add,x.statusCode=function(a){if(a){var b;if(v<2)for(b in a)r[b]=[r[b],a[b]];else b=a[x.status],x.always(b)}return this},l.url=((a||l.url)+"").replace(cl,"").replace(cp,ck[1]+"//"),l.dataTypes=p.trim(l.dataType||"*").toLowerCase().split(s),l.crossDomain==null&&(i=ct.exec(l.url.toLowerCase()),l.crossDomain=!(!i||i[1]==ck[1]&&i[2]==ck[2]&&(i[3]||(i[1]==="http:"?80:443))==(ck[3]||(ck[1]==="http:"?80:443)))),l.data&&l.processData&&typeof l.data!="string"&&(l.data=p.param(l.data,l.traditional)),cA(cv,l,c,x);if(v===2)return x;j=l.global,l.type=l.type.toUpperCase(),l.hasContent=!co.test(l.type),j&&p.active++===0&&p.event.trigger("ajaxStart");if(!l.hasContent){l.data&&(l.url+=(cq.test(l.url)?"&":"?")+l.data,delete l.data),d=l.url;if(l.cache===!1){var z=p.now(),A=l.url.replace(cs,"$1_="+z);l.url=A+(A===l.url?(cq.test(l.url)?"&":"?")+"_="+z:"")}}(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&x.setRequestHeader("Content-Type",l.contentType),l.ifModified&&(d=d||l.url,p.lastModified[d]&&x.setRequestHeader("If-Modified-Since",p.lastModified[d]),p.etag[d]&&x.setRequestHeader("If-None-Match",p.etag[d])),x.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+(l.dataTypes[0]!=="*"?", "+cx+"; q=0.01":""):l.accepts["*"]);for(k in l.headers)x.setRequestHeader(k,l.headers[k]);if(!l.beforeSend||l.beforeSend.call(m,x,l)!==!1&&v!==2){w="abort";for(k in{success:1,error:1,complete:1})x[k](l[k]);g=cA(cw,l,c,x);if(!g)y(-1,"No Transport");else{x.readyState=1,j&&n.trigger("ajaxSend",[x,l]),l.async&&l.timeout>0&&(h=setTimeout(function(){x.abort("timeout")},l.timeout));try{v=1,g.send(t,y)}catch(B){if(v<2)y(-1,B);else throw B}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var cE=[],cF=/\?/,cG=/(=)\?(?=&|$)|\?\?/,cH=p.now();p.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=cE.pop()||p.expando+"_"+cH++;return this[a]=!0,a}}),p.ajaxPrefilter("json jsonp",function(c,d,e){var f,g,h,i=c.data,j=c.url,k=c.jsonp!==!1,l=k&&cG.test(j),m=k&&!l&&typeof i=="string"&&!(c.contentType||"").indexOf("application/x-www-form-urlencoded")&&cG.test(i);if(c.dataTypes[0]==="jsonp"||l||m)return f=c.jsonpCallback=p.isFunction(c.jsonpCallback)?c.jsonpCallback():c.jsonpCallback,g=a[f],l?c.url=j.replace(cG,"$1"+f):m?c.data=i.replace(cG,"$1"+f):k&&(c.url+=(cF.test(j)?"&":"?")+c.jsonp+"="+f),c.converters["script json"]=function(){return h||p.error(f+" was not called"),h[0]},c.dataTypes[0]="json",a[f]=function(){h=arguments},e.always(function(){a[f]=g,c[f]&&(c.jsonpCallback=d.jsonpCallback,cE.push(f)),h&&p.isFunction(g)&&g(h[0]),h=g=b}),"script"}),p.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){return p.globalEval(a),a}}}),p.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),p.ajaxTransport("script",function(a){if(a.crossDomain){var c,d=e.head||e.getElementsByTagName("head")[0]||e.documentElement;return{send:function(f,g){c=e.createElement("script"),c.async="async",a.scriptCharset&&(c.charset=a.scriptCharset),c.src=a.url,c.onload=c.onreadystatechange=function(a,e){if(e||!c.readyState||/loaded|complete/.test(c.readyState))c.onload=c.onreadystatechange=null,d&&c.parentNode&&d.removeChild(c),c=b,e||g(200,"success")},d.insertBefore(c,d.firstChild)},abort:function(){c&&c.onload(0,1)}}}});var cI,cJ=a.ActiveXObject?function(){for(var a in cI)cI[a](0,1)}:!1,cK=0;p.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cL()||cM()}:cL,function(a){p.extend(p.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(p.ajaxSettings.xhr()),p.support.ajax&&p.ajaxTransport(function(c){if(!c.crossDomain||p.support.cors){var d;return{send:function(e,f){var g,h,i=c.xhr();c.username?i.open(c.type,c.url,c.async,c.username,c.password):i.open(c.type,c.url,c.async);if(c.xhrFields)for(h in c.xhrFields)i[h]=c.xhrFields[h];c.mimeType&&i.overrideMimeType&&i.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(h in e)i.setRequestHeader(h,e[h])}catch(j){}i.send(c.hasContent&&c.data||null),d=function(a,e){var h,j,k,l,m;try{if(d&&(e||i.readyState===4)){d=b,g&&(i.onreadystatechange=p.noop,cJ&&delete cI[g]);if(e)i.readyState!==4&&i.abort();else{h=i.status,k=i.getAllResponseHeaders(),l={},m=i.responseXML,m&&m.documentElement&&(l.xml=m);try{l.text=i.responseText}catch(a){}try{j=i.statusText}catch(n){j=""}!h&&c.isLocal&&!c.crossDomain?h=l.text?200:404:h===1223&&(h=204)}}}catch(o){e||f(-1,o)}l&&f(h,j,l,k)},c.async?i.readyState===4?setTimeout(d,0):(g=++cK,cJ&&(cI||(cI={},p(a).unload(cJ)),cI[g]=d),i.onreadystatechange=d):d()},abort:function(){d&&d(0,1)}}}});var cN,cO,cP=/^(?:toggle|show|hide)$/,cQ=new RegExp("^(?:([-+])=|)("+q+")([a-z%]*)$","i"),cR=/queueHooks$/,cS=[cY],cT={"*":[function(a,b){var c,d,e,f=this.createTween(a,b),g=cQ.exec(b),h=f.cur(),i=+h||0,j=1;if(g){c=+g[2],d=g[3]||(p.cssNumber[a]?"":"px");if(d!=="px"&&i){i=p.css(f.elem,a,!0)||c||1;do e=j=j||".5",i=i/j,p.style(f.elem,a,i+d),j=f.cur()/h;while(j!==1&&j!==e)}f.unit=d,f.start=i,f.end=g[1]?i+(g[1]+1)*c:c}return f}]};p.Animation=p.extend(cW,{tweener:function(a,b){p.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");var c,d=0,e=a.length;for(;d-1,j={},k={},l,m;i?(k=e.position(),l=k.top,m=k.left):(l=parseFloat(g)||0,m=parseFloat(h)||0),p.isFunction(b)&&(b=b.call(a,c,f)),b.top!=null&&(j.top=b.top-f.top+l),b.left!=null&&(j.left=b.left-f.left+m),"using"in b?b.using.call(a,j):e.css(j)}},p.fn.extend({position:function(){if(!this[0])return;var a=this[0],b=this.offsetParent(),c=this.offset(),d=c_.test(b[0].nodeName)?{top:0,left:0}:b.offset();return c.top-=parseFloat(p.css(a,"marginTop"))||0,c.left-=parseFloat(p.css(a,"marginLeft"))||0,d.top+=parseFloat(p.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(p.css(b[0],"borderLeftWidth"))||0,{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||e.body;while(a&&!c_.test(a.nodeName)&&p.css(a,"position")==="static")a=a.offsetParent;return a||e.body})}}),p.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);p.fn[a]=function(e){return p.access(this,function(a,e,f){var g=da(a);if(f===b)return g?c in g?g[c]:g.document.documentElement[e]:a[e];g?g.scrollTo(d?p(g).scrollLeft():f,d?f:p(g).scrollTop()):a[e]=f},a,e,arguments.length,null)}}),p.each({Height:"height",Width:"width"},function(a,c){p.each({padding:"inner"+a,content:c,"":"outer"+a},function(d,e){p.fn[e]=function(e,f){var g=arguments.length&&(d||typeof e!="boolean"),h=d||(e===!0||f===!0?"margin":"border");return p.access(this,function(c,d,e){var f;return p.isWindow(c)?c.document.documentElement["client"+a]:c.nodeType===9?(f=c.documentElement,Math.max(c.body["scroll"+a],f["scroll"+a],c.body["offset"+a],f["offset"+a],f["client"+a])):e===b?p.css(c,d,e,h):p.style(c,d,e,h)},c,g?e:b,g,null)}})}),a.jQuery=a.$=p,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return p})})(window); \ No newline at end of file diff --git a/sitestatic/jquery-1.8.2.min.js b/sitestatic/jquery-1.8.2.min.js new file mode 100644 index 00000000..f65cf1dc --- /dev/null +++ b/sitestatic/jquery-1.8.2.min.js @@ -0,0 +1,2 @@ +/*! jQuery v1.8.2 jquery.com | jquery.org/license */ +(function(a,b){function G(a){var b=F[a]={};return p.each(a.split(s),function(a,c){b[c]=!0}),b}function J(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(I,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:+d+""===d?+d:H.test(d)?p.parseJSON(d):d}catch(f){}p.data(a,c,d)}else d=b}return d}function K(a){var b;for(b in a){if(b==="data"&&p.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function ba(){return!1}function bb(){return!0}function bh(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function bi(a,b){do a=a[b];while(a&&a.nodeType!==1);return a}function bj(a,b,c){b=b||0;if(p.isFunction(b))return p.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return p.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=p.grep(a,function(a){return a.nodeType===1});if(be.test(b))return p.filter(b,d,!c);b=p.filter(b,d)}return p.grep(a,function(a,d){return p.inArray(a,b)>=0===c})}function bk(a){var b=bl.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function bC(a,b){return a.getElementsByTagName(b)[0]||a.appendChild(a.ownerDocument.createElement(b))}function bD(a,b){if(b.nodeType!==1||!p.hasData(a))return;var c,d,e,f=p._data(a),g=p._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;d").appendTo(e.body),c=b.css("display");b.remove();if(c==="none"||c===""){bI=e.body.appendChild(bI||p.extend(e.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!bJ||!bI.createElement)bJ=(bI.contentWindow||bI.contentDocument).document,bJ.write(""),bJ.close();b=bJ.body.appendChild(bJ.createElement(a)),c=bH(b,"display"),e.body.removeChild(bI)}return bS[a]=c,c}function ci(a,b,c,d){var e;if(p.isArray(b))p.each(b,function(b,e){c||ce.test(a)?d(a,e):ci(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&p.type(b)==="object")for(e in b)ci(a+"["+e+"]",b[e],c,d);else d(a,b)}function cz(a){return function(b,c){typeof b!="string"&&(c=b,b="*");var d,e,f,g=b.toLowerCase().split(s),h=0,i=g.length;if(p.isFunction(c))for(;h)[^>]*$|#([\w\-]*)$)/,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,y=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,z=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,A=/^-ms-/,B=/-([\da-z])/gi,C=function(a,b){return(b+"").toUpperCase()},D=function(){e.addEventListener?(e.removeEventListener("DOMContentLoaded",D,!1),p.ready()):e.readyState==="complete"&&(e.detachEvent("onreadystatechange",D),p.ready())},E={};p.fn=p.prototype={constructor:p,init:function(a,c,d){var f,g,h,i;if(!a)return this;if(a.nodeType)return this.context=this[0]=a,this.length=1,this;if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?f=[null,a,null]:f=u.exec(a);if(f&&(f[1]||!c)){if(f[1])return c=c instanceof p?c[0]:c,i=c&&c.nodeType?c.ownerDocument||c:e,a=p.parseHTML(f[1],i,!0),v.test(f[1])&&p.isPlainObject(c)&&this.attr.call(a,c,!0),p.merge(this,a);g=e.getElementById(f[2]);if(g&&g.parentNode){if(g.id!==f[2])return d.find(a);this.length=1,this[0]=g}return this.context=e,this.selector=a,this}return!c||c.jquery?(c||d).find(a):this.constructor(c).find(a)}return p.isFunction(a)?d.ready(a):(a.selector!==b&&(this.selector=a.selector,this.context=a.context),p.makeArray(a,this))},selector:"",jquery:"1.8.2",length:0,size:function(){return this.length},toArray:function(){return k.call(this)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=p.merge(this.constructor(),a);return d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")"),d},each:function(a,b){return p.each(this,a,b)},ready:function(a){return p.ready.promise().done(a),this},eq:function(a){return a=+a,a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(k.apply(this,arguments),"slice",k.call(arguments).join(","))},map:function(a){return this.pushStack(p.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:j,sort:[].sort,splice:[].splice},p.fn.init.prototype=p.fn,p.extend=p.fn.extend=function(){var a,c,d,e,f,g,h=arguments[0]||{},i=1,j=arguments.length,k=!1;typeof h=="boolean"&&(k=h,h=arguments[1]||{},i=2),typeof h!="object"&&!p.isFunction(h)&&(h={}),j===i&&(h=this,--i);for(;i0)return;d.resolveWith(e,[p]),p.fn.trigger&&p(e).trigger("ready").off("ready")},isFunction:function(a){return p.type(a)==="function"},isArray:Array.isArray||function(a){return p.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):E[m.call(a)]||"object"},isPlainObject:function(a){if(!a||p.type(a)!=="object"||a.nodeType||p.isWindow(a))return!1;try{if(a.constructor&&!n.call(a,"constructor")&&!n.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||n.call(a,d)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},error:function(a){throw new Error(a)},parseHTML:function(a,b,c){var d;return!a||typeof a!="string"?null:(typeof b=="boolean"&&(c=b,b=0),b=b||e,(d=v.exec(a))?[b.createElement(d[1])]:(d=p.buildFragment([a],b,c?null:[]),p.merge([],(d.cacheable?p.clone(d.fragment):d.fragment).childNodes)))},parseJSON:function(b){if(!b||typeof b!="string")return null;b=p.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(w.test(b.replace(y,"@").replace(z,"]").replace(x,"")))return(new Function("return "+b))();p.error("Invalid JSON: "+b)},parseXML:function(c){var d,e;if(!c||typeof c!="string")return null;try{a.DOMParser?(e=new DOMParser,d=e.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(f){d=b}return(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&p.error("Invalid XML: "+c),d},noop:function(){},globalEval:function(b){b&&r.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(A,"ms-").replace(B,C)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,c,d){var e,f=0,g=a.length,h=g===b||p.isFunction(a);if(d){if(h){for(e in a)if(c.apply(a[e],d)===!1)break}else for(;f0&&a[0]&&a[i-1]||i===0||p.isArray(a));if(j)for(;h-1)i.splice(c,1),e&&(c<=g&&g--,c<=h&&h--)}),this},has:function(a){return p.inArray(a,i)>-1},empty:function(){return i=[],this},disable:function(){return i=j=c=b,this},disabled:function(){return!i},lock:function(){return j=b,c||l.disable(),this},locked:function(){return!j},fireWith:function(a,b){return b=b||[],b=[a,b.slice?b.slice():b],i&&(!d||j)&&(e?j.push(b):k(b)),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!d}};return l},p.extend({Deferred:function(a){var b=[["resolve","done",p.Callbacks("once memory"),"resolved"],["reject","fail",p.Callbacks("once memory"),"rejected"],["notify","progress",p.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return p.Deferred(function(c){p.each(b,function(b,d){var f=d[0],g=a[b];e[d[1]](p.isFunction(g)?function(){var a=g.apply(this,arguments);a&&p.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f+"With"](this===e?c:this,[a])}:c[f])}),a=null}).promise()},promise:function(a){return a!=null?p.extend(a,d):d}},e={};return d.pipe=d.then,p.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[a^1][2].disable,b[2][2].lock),e[f[0]]=g.fire,e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=k.call(arguments),d=c.length,e=d!==1||a&&p.isFunction(a.promise)?d:0,f=e===1?a:p.Deferred(),g=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?k.call(arguments):d,c===h?f.notifyWith(b,c):--e||f.resolveWith(b,c)}},h,i,j;if(d>1){h=new Array(d),i=new Array(d),j=new Array(d);for(;b
    a",c=n.getElementsByTagName("*"),d=n.getElementsByTagName("a")[0],d.style.cssText="top:1px;float:left;opacity:.5";if(!c||!c.length)return{};f=e.createElement("select"),g=f.appendChild(e.createElement("option")),h=n.getElementsByTagName("input")[0],b={leadingWhitespace:n.firstChild.nodeType===3,tbody:!n.getElementsByTagName("tbody").length,htmlSerialize:!!n.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.5/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:h.value==="on",optSelected:g.selected,getSetAttribute:n.className!=="t",enctype:!!e.createElement("form").enctype,html5Clone:e.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",boxModel:e.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},h.checked=!0,b.noCloneChecked=h.cloneNode(!0).checked,f.disabled=!0,b.optDisabled=!g.disabled;try{delete n.test}catch(o){b.deleteExpando=!1}!n.addEventListener&&n.attachEvent&&n.fireEvent&&(n.attachEvent("onclick",m=function(){b.noCloneEvent=!1}),n.cloneNode(!0).fireEvent("onclick"),n.detachEvent("onclick",m)),h=e.createElement("input"),h.value="t",h.setAttribute("type","radio"),b.radioValue=h.value==="t",h.setAttribute("checked","checked"),h.setAttribute("name","t"),n.appendChild(h),i=e.createDocumentFragment(),i.appendChild(n.lastChild),b.checkClone=i.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=h.checked,i.removeChild(h),i.appendChild(n);if(n.attachEvent)for(k in{submit:!0,change:!0,focusin:!0})j="on"+k,l=j in n,l||(n.setAttribute(j,"return;"),l=typeof n[j]=="function"),b[k+"Bubbles"]=l;return p(function(){var c,d,f,g,h="padding:0;margin:0;border:0;display:block;overflow:hidden;",i=e.getElementsByTagName("body")[0];if(!i)return;c=e.createElement("div"),c.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",i.insertBefore(c,i.firstChild),d=e.createElement("div"),c.appendChild(d),d.innerHTML="
    t
    ",f=d.getElementsByTagName("td"),f[0].style.cssText="padding:0;margin:0;border:0;display:none",l=f[0].offsetHeight===0,f[0].style.display="",f[1].style.display="none",b.reliableHiddenOffsets=l&&f[0].offsetHeight===0,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",b.boxSizing=d.offsetWidth===4,b.doesNotIncludeMarginInBodyOffset=i.offsetTop!==1,a.getComputedStyle&&(b.pixelPosition=(a.getComputedStyle(d,null)||{}).top!=="1%",b.boxSizingReliable=(a.getComputedStyle(d,null)||{width:"4px"}).width==="4px",g=e.createElement("div"),g.style.cssText=d.style.cssText=h,g.style.marginRight=g.style.width="0",d.style.width="1px",d.appendChild(g),b.reliableMarginRight=!parseFloat((a.getComputedStyle(g,null)||{}).marginRight)),typeof d.style.zoom!="undefined"&&(d.innerHTML="",d.style.cssText=h+"width:1px;padding:1px;display:inline;zoom:1",b.inlineBlockNeedsLayout=d.offsetWidth===3,d.style.display="block",d.style.overflow="visible",d.innerHTML="
    ",d.firstChild.style.width="5px",b.shrinkWrapBlocks=d.offsetWidth!==3,c.style.zoom=1),i.removeChild(c),c=d=f=g=null}),i.removeChild(n),c=d=f=g=h=i=n=null,b}();var H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,I=/([A-Z])/g;p.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(p.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){return a=a.nodeType?p.cache[a[p.expando]]:a[p.expando],!!a&&!K(a)},data:function(a,c,d,e){if(!p.acceptData(a))return;var f,g,h=p.expando,i=typeof c=="string",j=a.nodeType,k=j?p.cache:a,l=j?a[h]:a[h]&&h;if((!l||!k[l]||!e&&!k[l].data)&&i&&d===b)return;l||(j?a[h]=l=p.deletedIds.pop()||p.guid++:l=h),k[l]||(k[l]={},j||(k[l].toJSON=p.noop));if(typeof c=="object"||typeof c=="function")e?k[l]=p.extend(k[l],c):k[l].data=p.extend(k[l].data,c);return f=k[l],e||(f.data||(f.data={}),f=f.data),d!==b&&(f[p.camelCase(c)]=d),i?(g=f[c],g==null&&(g=f[p.camelCase(c)])):g=f,g},removeData:function(a,b,c){if(!p.acceptData(a))return;var d,e,f,g=a.nodeType,h=g?p.cache:a,i=g?a[p.expando]:p.expando;if(!h[i])return;if(b){d=c?h[i]:h[i].data;if(d){p.isArray(b)||(b in d?b=[b]:(b=p.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,f=b.length;e1,null,!1))},removeData:function(a){return this.each(function(){p.removeData(this,a)})}}),p.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=p._data(a,b),c&&(!d||p.isArray(c)?d=p._data(a,b,p.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=p.queue(a,b),d=c.length,e=c.shift(),f=p._queueHooks(a,b),g=function(){p.dequeue(a,b)};e==="inprogress"&&(e=c.shift(),d--),e&&(b==="fx"&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return p._data(a,c)||p._data(a,c,{empty:p.Callbacks("once memory").add(function(){p.removeData(a,b+"queue",!0),p.removeData(a,c,!0)})})}}),p.fn.extend({queue:function(a,c){var d=2;return typeof a!="string"&&(c=a,a="fx",d--),arguments.length1)},removeAttr:function(a){return this.each(function(){p.removeAttr(this,a)})},prop:function(a,b){return p.access(this,p.prop,a,b,arguments.length>1)},removeProp:function(a){return a=p.propFix[a]||a,this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,f,g,h;if(p.isFunction(a))return this.each(function(b){p(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(s);for(c=0,d=this.length;c=0)d=d.replace(" "+c[f]+" "," ");e.className=a?p.trim(d):""}}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";return p.isFunction(a)?this.each(function(c){p(this).toggleClass(a.call(this,c,this.className,b),b)}):this.each(function(){if(c==="string"){var e,f=0,g=p(this),h=b,i=a.split(s);while(e=i[f++])h=d?h:!g.hasClass(e),g[h?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&p._data(this,"__className__",this.className),this.className=this.className||a===!1?"":p._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c=0)return!0;return!1},val:function(a){var c,d,e,f=this[0];if(!arguments.length){if(f)return c=p.valHooks[f.type]||p.valHooks[f.nodeName.toLowerCase()],c&&"get"in c&&(d=c.get(f,"value"))!==b?d:(d=f.value,typeof d=="string"?d.replace(P,""):d==null?"":d);return}return e=p.isFunction(a),this.each(function(d){var f,g=p(this);if(this.nodeType!==1)return;e?f=a.call(this,d,g.val()):f=a,f==null?f="":typeof f=="number"?f+="":p.isArray(f)&&(f=p.map(f,function(a){return a==null?"":a+""})),c=p.valHooks[this.type]||p.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,f,"value")===b)this.value=f})}}),p.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,f=a.selectedIndex,g=[],h=a.options,i=a.type==="select-one";if(f<0)return null;c=i?f:0,d=i?f+1:h.length;for(;c=0}),c.length||(a.selectedIndex=-1),c}}},attrFn:{},attr:function(a,c,d,e){var f,g,h,i=a.nodeType;if(!a||i===3||i===8||i===2)return;if(e&&p.isFunction(p.fn[c]))return p(a)[c](d);if(typeof a.getAttribute=="undefined")return p.prop(a,c,d);h=i!==1||!p.isXMLDoc(a),h&&(c=c.toLowerCase(),g=p.attrHooks[c]||(T.test(c)?M:L));if(d!==b){if(d===null){p.removeAttr(a,c);return}return g&&"set"in g&&h&&(f=g.set(a,d,c))!==b?f:(a.setAttribute(c,d+""),d)}return g&&"get"in g&&h&&(f=g.get(a,c))!==null?f:(f=a.getAttribute(c),f===null?b:f)},removeAttr:function(a,b){var c,d,e,f,g=0;if(b&&a.nodeType===1){d=b.split(s);for(;g=0}})});var V=/^(?:textarea|input|select)$/i,W=/^([^\.]*|)(?:\.(.+)|)$/,X=/(?:^|\s)hover(\.\S+|)\b/,Y=/^key/,Z=/^(?:mouse|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=function(a){return p.event.special.hover?a:a.replace(X,"mouseenter$1 mouseleave$1")};p.event={add:function(a,c,d,e,f){var g,h,i,j,k,l,m,n,o,q,r;if(a.nodeType===3||a.nodeType===8||!c||!d||!(g=p._data(a)))return;d.handler&&(o=d,d=o.handler,f=o.selector),d.guid||(d.guid=p.guid++),i=g.events,i||(g.events=i={}),h=g.handle,h||(g.handle=h=function(a){return typeof p!="undefined"&&(!a||p.event.triggered!==a.type)?p.event.dispatch.apply(h.elem,arguments):b},h.elem=a),c=p.trim(_(c)).split(" ");for(j=0;j=0&&(s=s.slice(0,-1),i=!0),s.indexOf(".")>=0&&(t=s.split("."),s=t.shift(),t.sort());if((!f||p.event.customEvent[s])&&!p.event.global[s])return;c=typeof c=="object"?c[p.expando]?c:new p.Event(s,c):new p.Event(s),c.type=s,c.isTrigger=!0,c.exclusive=i,c.namespace=t.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+t.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,m=s.indexOf(":")<0?"on"+s:"";if(!f){h=p.cache;for(j in h)h[j].events&&h[j].events[s]&&p.event.trigger(c,d,h[j].handle.elem,!0);return}c.result=b,c.target||(c.target=f),d=d!=null?p.makeArray(d):[],d.unshift(c),n=p.event.special[s]||{};if(n.trigger&&n.trigger.apply(f,d)===!1)return;q=[[f,n.bindType||s]];if(!g&&!n.noBubble&&!p.isWindow(f)){r=n.delegateType||s,k=$.test(r+s)?f:f.parentNode;for(l=f;k;k=k.parentNode)q.push([k,r]),l=k;l===(f.ownerDocument||e)&&q.push([l.defaultView||l.parentWindow||a,r])}for(j=0;j=0:p.find(m,this,null,[f]).length),h[m]&&j.push(l);j.length&&u.push({elem:f,matches:j})}o.length>q&&u.push({elem:this,matches:o.slice(q)});for(d=0;d0?this.on(b,null,a,c):this.trigger(b)},Y.test(b)&&(p.event.fixHooks[b]=p.event.keyHooks),Z.test(b)&&(p.event.fixHooks[b]=p.event.mouseHooks)}),function(a,b){function bc(a,b,c,d){c=c||[],b=b||r;var e,f,i,j,k=b.nodeType;if(!a||typeof a!="string")return c;if(k!==1&&k!==9)return[];i=g(b);if(!i&&!d)if(e=P.exec(a))if(j=e[1]){if(k===9){f=b.getElementById(j);if(!f||!f.parentNode)return c;if(f.id===j)return c.push(f),c}else if(b.ownerDocument&&(f=b.ownerDocument.getElementById(j))&&h(b,f)&&f.id===j)return c.push(f),c}else{if(e[2])return w.apply(c,x.call(b.getElementsByTagName(a),0)),c;if((j=e[3])&&_&&b.getElementsByClassName)return w.apply(c,x.call(b.getElementsByClassName(j),0)),c}return bp(a.replace(L,"$1"),b,c,d,i)}function bd(a){return function(b){var c=b.nodeName.toLowerCase();return c==="input"&&b.type===a}}function be(a){return function(b){var c=b.nodeName.toLowerCase();return(c==="input"||c==="button")&&b.type===a}}function bf(a){return z(function(b){return b=+b,z(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function bg(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}function bh(a,b){var c,d,f,g,h,i,j,k=C[o][a];if(k)return b?0:k.slice(0);h=a,i=[],j=e.preFilter;while(h){if(!c||(d=M.exec(h)))d&&(h=h.slice(d[0].length)),i.push(f=[]);c=!1;if(d=N.exec(h))f.push(c=new q(d.shift())),h=h.slice(c.length),c.type=d[0].replace(L," ");for(g in e.filter)(d=W[g].exec(h))&&(!j[g]||(d=j[g](d,r,!0)))&&(f.push(c=new q(d.shift())),h=h.slice(c.length),c.type=g,c.matches=d);if(!c)break}return b?h.length:h?bc.error(a):C(a,i).slice(0)}function bi(a,b,d){var e=b.dir,f=d&&b.dir==="parentNode",g=u++;return b.first?function(b,c,d){while(b=b[e])if(f||b.nodeType===1)return a(b,c,d)}:function(b,d,h){if(!h){var i,j=t+" "+g+" ",k=j+c;while(b=b[e])if(f||b.nodeType===1){if((i=b[o])===k)return b.sizset;if(typeof i=="string"&&i.indexOf(j)===0){if(b.sizset)return b}else{b[o]=k;if(a(b,d,h))return b.sizset=!0,b;b.sizset=!1}}}else while(b=b[e])if(f||b.nodeType===1)if(a(b,d,h))return b}}function bj(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function bk(a,b,c,d,e){var f,g=[],h=0,i=a.length,j=b!=null;for(;h-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==l)||((b=c).nodeType?j(a,c,d):k(a,c,d))}];for(;i1&&bj(m),i>1&&a.slice(0,i-1).join("").replace(L,"$1"),c,i0,f=a.length>0,g=function(h,i,j,k,m){var n,o,p,q=[],s=0,u="0",x=h&&[],y=m!=null,z=l,A=h||f&&e.find.TAG("*",m&&i.parentNode||i),B=t+=z==null?1:Math.E;y&&(l=i!==r&&i,c=g.el);for(;(n=A[u])!=null;u++){if(f&&n){for(o=0;p=a[o];o++)if(p(n,i,j)){k.push(n);break}y&&(t=B,c=++g.el)}d&&((n=!p&&n)&&s--,h&&x.push(n))}s+=u;if(d&&u!==s){for(o=0;p=b[o];o++)p(x,q,i,j);if(h){if(s>0)while(u--)!x[u]&&!q[u]&&(q[u]=v.call(k));q=bk(q)}w.apply(k,q),y&&!h&&q.length>0&&s+b.length>1&&bc.uniqueSort(k)}return y&&(t=B,l=z),x};return g.el=0,d?z(g):g}function bo(a,b,c,d){var e=0,f=b.length;for(;e2&&(j=h[0]).type==="ID"&&b.nodeType===9&&!f&&e.relative[h[1].type]){b=e.find.ID(j.matches[0].replace(V,""),b,f)[0];if(!b)return c;a=a.slice(h.shift().length)}for(g=W.POS.test(a)?-1:h.length-1;g>=0;g--){j=h[g];if(e.relative[k=j.type])break;if(l=e.find[k])if(d=l(j.matches[0].replace(V,""),R.test(h[0].type)&&b.parentNode||b,f)){h.splice(g,1),a=d.length&&h.join("");if(!a)return w.apply(c,x.call(d,0)),c;break}}}return i(a,m)(d,b,f,c,R.test(a)),c}function bq(){}var c,d,e,f,g,h,i,j,k,l,m=!0,n="undefined",o=("sizcache"+Math.random()).replace(".",""),q=String,r=a.document,s=r.documentElement,t=0,u=0,v=[].pop,w=[].push,x=[].slice,y=[].indexOf||function(a){var b=0,c=this.length;for(;be.cacheLength&&delete a[b.shift()],a[c]=d},a)},B=A(),C=A(),D=A(),E="[\\x20\\t\\r\\n\\f]",F="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",G=F.replace("w","w#"),H="([*^$|!~]?=)",I="\\["+E+"*("+F+")"+E+"*(?:"+H+E+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+G+")|)|)"+E+"*\\]",J=":("+F+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+I+")|[^:]|\\\\.)*|.*))\\)|)",K=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+E+"*((?:-\\d)?\\d*)"+E+"*\\)|)(?=[^-]|$)",L=new RegExp("^"+E+"+|((?:^|[^\\\\])(?:\\\\.)*)"+E+"+$","g"),M=new RegExp("^"+E+"*,"+E+"*"),N=new RegExp("^"+E+"*([\\x20\\t\\r\\n\\f>+~])"+E+"*"),O=new RegExp(J),P=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,Q=/^:not/,R=/[\x20\t\r\n\f]*[+~]/,S=/:not\($/,T=/h\d/i,U=/input|select|textarea|button/i,V=/\\(?!\\)/g,W={ID:new RegExp("^#("+F+")"),CLASS:new RegExp("^\\.("+F+")"),NAME:new RegExp("^\\[name=['\"]?("+F+")['\"]?\\]"),TAG:new RegExp("^("+F.replace("w","w*")+")"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+J),POS:new RegExp(K,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+E+"*(even|odd|(([+-]|)(\\d*)n|)"+E+"*(?:([+-]|)"+E+"*(\\d+)|))"+E+"*\\)|)","i"),needsContext:new RegExp("^"+E+"*[>+~]|"+K,"i")},X=function(a){var b=r.createElement("div");try{return a(b)}catch(c){return!1}finally{b=null}},Y=X(function(a){return a.appendChild(r.createComment("")),!a.getElementsByTagName("*").length}),Z=X(function(a){return a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!==n&&a.firstChild.getAttribute("href")==="#"}),$=X(function(a){a.innerHTML="";var b=typeof a.lastChild.getAttribute("multiple");return b!=="boolean"&&b!=="string"}),_=X(function(a){return a.innerHTML="",!a.getElementsByClassName||!a.getElementsByClassName("e").length?!1:(a.lastChild.className="e",a.getElementsByClassName("e").length===2)}),ba=X(function(a){a.id=o+0,a.innerHTML="
    ",s.insertBefore(a,s.firstChild);var b=r.getElementsByName&&r.getElementsByName(o).length===2+r.getElementsByName(o+0).length;return d=!r.getElementById(o),s.removeChild(a),b});try{x.call(s.childNodes,0)[0].nodeType}catch(bb){x=function(a){var b,c=[];for(;b=this[a];a++)c.push(b);return c}}bc.matches=function(a,b){return bc(a,null,null,b)},bc.matchesSelector=function(a,b){return bc(b,null,null,[a]).length>0},f=bc.getText=function(a){var b,c="",d=0,e=a.nodeType;if(e){if(e===1||e===9||e===11){if(typeof a.textContent=="string")return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=f(a)}else if(e===3||e===4)return a.nodeValue}else for(;b=a[d];d++)c+=f(b);return c},g=bc.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?b.nodeName!=="HTML":!1},h=bc.contains=s.contains?function(a,b){var c=a.nodeType===9?a.documentElement:a,d=b&&b.parentNode;return a===d||!!(d&&d.nodeType===1&&c.contains&&c.contains(d))}:s.compareDocumentPosition?function(a,b){return b&&!!(a.compareDocumentPosition(b)&16)}:function(a,b){while(b=b.parentNode)if(b===a)return!0;return!1},bc.attr=function(a,b){var c,d=g(a);return d||(b=b.toLowerCase()),(c=e.attrHandle[b])?c(a):d||$?a.getAttribute(b):(c=a.getAttributeNode(b),c?typeof a[b]=="boolean"?a[b]?b:null:c.specified?c.value:null:null)},e=bc.selectors={cacheLength:50,createPseudo:z,match:W,attrHandle:Z?{}:{href:function(a){return a.getAttribute("href",2)},type:function(a){return a.getAttribute("type")}},find:{ID:d?function(a,b,c){if(typeof b.getElementById!==n&&!c){var d=b.getElementById(a);return d&&d.parentNode?[d]:[]}}:function(a,c,d){if(typeof c.getElementById!==n&&!d){var e=c.getElementById(a);return e?e.id===a||typeof e.getAttributeNode!==n&&e.getAttributeNode("id").value===a?[e]:b:[]}},TAG:Y?function(a,b){if(typeof b.getElementsByTagName!==n)return b.getElementsByTagName(a)}:function(a,b){var c=b.getElementsByTagName(a);if(a==="*"){var d,e=[],f=0;for(;d=c[f];f++)d.nodeType===1&&e.push(d);return e}return c},NAME:ba&&function(a,b){if(typeof b.getElementsByName!==n)return b.getElementsByName(name)},CLASS:_&&function(a,b,c){if(typeof b.getElementsByClassName!==n&&!c)return b.getElementsByClassName(a)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(V,""),a[3]=(a[4]||a[5]||"").replace(V,""),a[2]==="~="&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),a[1]==="nth"?(a[2]||bc.error(a[0]),a[3]=+(a[3]?a[4]+(a[5]||1):2*(a[2]==="even"||a[2]==="odd")),a[4]=+(a[6]+a[7]||a[2]==="odd")):a[2]&&bc.error(a[0]),a},PSEUDO:function(a){var b,c;if(W.CHILD.test(a[0]))return null;if(a[3])a[2]=a[3];else if(b=a[4])O.test(b)&&(c=bh(b,!0))&&(c=b.indexOf(")",b.length-c)-b.length)&&(b=b.slice(0,c),a[0]=a[0].slice(0,c)),a[2]=b;return a.slice(0,3)}},filter:{ID:d?function(a){return a=a.replace(V,""),function(b){return b.getAttribute("id")===a}}:function(a){return a=a.replace(V,""),function(b){var c=typeof b.getAttributeNode!==n&&b.getAttributeNode("id");return c&&c.value===a}},TAG:function(a){return a==="*"?function(){return!0}:(a=a.replace(V,"").toLowerCase(),function(b){return b.nodeName&&b.nodeName.toLowerCase()===a})},CLASS:function(a){var b=B[o][a];return b||(b=B(a,new RegExp("(^|"+E+")"+a+"("+E+"|$)"))),function(a){return b.test(a.className||typeof a.getAttribute!==n&&a.getAttribute("class")||"")}},ATTR:function(a,b,c){return function(d,e){var f=bc.attr(d,a);return f==null?b==="!=":b?(f+="",b==="="?f===c:b==="!="?f!==c:b==="^="?c&&f.indexOf(c)===0:b==="*="?c&&f.indexOf(c)>-1:b==="$="?c&&f.substr(f.length-c.length)===c:b==="~="?(" "+f+" ").indexOf(c)>-1:b==="|="?f===c||f.substr(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d){return a==="nth"?function(a){var b,e,f=a.parentNode;if(c===1&&d===0)return!0;if(f){e=0;for(b=f.firstChild;b;b=b.nextSibling)if(b.nodeType===1){e++;if(a===b)break}}return e-=d,e===c||e%c===0&&e/c>=0}:function(b){var c=b;switch(a){case"only":case"first":while(c=c.previousSibling)if(c.nodeType===1)return!1;if(a==="first")return!0;c=b;case"last":while(c=c.nextSibling)if(c.nodeType===1)return!1;return!0}}},PSEUDO:function(a,b){var c,d=e.pseudos[a]||e.setFilters[a.toLowerCase()]||bc.error("unsupported pseudo: "+a);return d[o]?d(b):d.length>1?(c=[a,a,"",b],e.setFilters.hasOwnProperty(a.toLowerCase())?z(function(a,c){var e,f=d(a,b),g=f.length;while(g--)e=y.call(a,f[g]),a[e]=!(c[e]=f[g])}):function(a){return d(a,0,c)}):d}},pseudos:{not:z(function(a){var b=[],c=[],d=i(a.replace(L,"$1"));return d[o]?z(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)if(f=g[h])a[h]=!(b[h]=f)}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:z(function(a){return function(b){return bc(a,b).length>0}}),contains:z(function(a){return function(b){return(b.textContent||b.innerText||f(b)).indexOf(a)>-1}}),enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&!!a.checked||b==="option"&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},parent:function(a){return!e.pseudos.empty(a)},empty:function(a){var b;a=a.firstChild;while(a){if(a.nodeName>"@"||(b=a.nodeType)===3||b===4)return!1;a=a.nextSibling}return!0},header:function(a){return T.test(a.nodeName)},text:function(a){var b,c;return a.nodeName.toLowerCase()==="input"&&(b=a.type)==="text"&&((c=a.getAttribute("type"))==null||c.toLowerCase()===b)},radio:bd("radio"),checkbox:bd("checkbox"),file:bd("file"),password:bd("password"),image:bd("image"),submit:be("submit"),reset:be("reset"),button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&a.type==="button"||b==="button"},input:function(a){return U.test(a.nodeName)},focus:function(a){var b=a.ownerDocument;return a===b.activeElement&&(!b.hasFocus||b.hasFocus())&&(!!a.type||!!a.href)},active:function(a){return a===a.ownerDocument.activeElement},first:bf(function(a,b,c){return[0]}),last:bf(function(a,b,c){return[b-1]}),eq:bf(function(a,b,c){return[c<0?c+b:c]}),even:bf(function(a,b,c){for(var d=0;d=0;)a.push(d);return a}),gt:bf(function(a,b,c){for(var d=c<0?c+b:c;++d",a.querySelectorAll("[selected]").length||e.push("\\["+E+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),a.querySelectorAll(":checked").length||e.push(":checked")}),X(function(a){a.innerHTML="

    ",a.querySelectorAll("[test^='']").length&&e.push("[*^$]="+E+"*(?:\"\"|'')"),a.innerHTML="",a.querySelectorAll(":enabled").length||e.push(":enabled",":disabled")}),e=new RegExp(e.join("|")),bp=function(a,d,f,g,h){if(!g&&!h&&(!e||!e.test(a))){var i,j,k=!0,l=o,m=d,n=d.nodeType===9&&a;if(d.nodeType===1&&d.nodeName.toLowerCase()!=="object"){i=bh(a),(k=d.getAttribute("id"))?l=k.replace(c,"\\$&"):d.setAttribute("id",l),l="[id='"+l+"'] ",j=i.length;while(j--)i[j]=l+i[j].join("");m=R.test(a)&&d.parentNode||d,n=i.join(",")}if(n)try{return w.apply(f,x.call(m.querySelectorAll(n),0)),f}catch(p){}finally{k||d.removeAttribute("id")}}return b(a,d,f,g,h)},h&&(X(function(b){a=h.call(b,"div");try{h.call(b,"[test!='']:sizzle"),f.push("!=",J)}catch(c){}}),f=new RegExp(f.join("|")),bc.matchesSelector=function(b,c){c=c.replace(d,"='$1']");if(!g(b)&&!f.test(c)&&(!e||!e.test(c)))try{var i=h.call(b,c);if(i||a||b.document&&b.document.nodeType!==11)return i}catch(j){}return bc(c,null,null,[b]).length>0})}(),e.pseudos.nth=e.pseudos.eq,e.filters=bq.prototype=e.pseudos,e.setFilters=new bq,bc.attr=p.attr,p.find=bc,p.expr=bc.selectors,p.expr[":"]=p.expr.pseudos,p.unique=bc.uniqueSort,p.text=bc.getText,p.isXMLDoc=bc.isXML,p.contains=bc.contains}(a);var bc=/Until$/,bd=/^(?:parents|prev(?:Until|All))/,be=/^.[^:#\[\.,]*$/,bf=p.expr.match.needsContext,bg={children:!0,contents:!0,next:!0,prev:!0};p.fn.extend({find:function(a){var b,c,d,e,f,g,h=this;if(typeof a!="string")return p(a).filter(function(){for(b=0,c=h.length;b0)for(e=d;e=0:p.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c,d=0,e=this.length,f=[],g=bf.test(a)||typeof a!="string"?p(a,b||this.context):0;for(;d-1:p.find.matchesSelector(c,a)){f.push(c);break}c=c.parentNode}}return f=f.length>1?p.unique(f):f,this.pushStack(f,"closest",a)},index:function(a){return a?typeof a=="string"?p.inArray(this[0],p(a)):p.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(a,b){var c=typeof a=="string"?p(a,b):p.makeArray(a&&a.nodeType?[a]:a),d=p.merge(this.get(),c);return this.pushStack(bh(c[0])||bh(d[0])?d:p.unique(d))},addBack:function(a){return this.add(a==null?this.prevObject:this.prevObject.filter(a))}}),p.fn.andSelf=p.fn.addBack,p.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return p.dir(a,"parentNode")},parentsUntil:function(a,b,c){return p.dir(a,"parentNode",c)},next:function(a){return bi(a,"nextSibling")},prev:function(a){return bi(a,"previousSibling")},nextAll:function(a){return p.dir(a,"nextSibling")},prevAll:function(a){return p.dir(a,"previousSibling")},nextUntil:function(a,b,c){return p.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return p.dir(a,"previousSibling",c)},siblings:function(a){return p.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return p.sibling(a.firstChild)},contents:function(a){return p.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:p.merge([],a.childNodes)}},function(a,b){p.fn[a]=function(c,d){var e=p.map(this,b,c);return bc.test(a)||(d=c),d&&typeof d=="string"&&(e=p.filter(d,e)),e=this.length>1&&!bg[a]?p.unique(e):e,this.length>1&&bd.test(a)&&(e=e.reverse()),this.pushStack(e,a,k.call(arguments).join(","))}}),p.extend({filter:function(a,b,c){return c&&(a=":not("+a+")"),b.length===1?p.find.matchesSelector(b[0],a)?[b[0]]:[]:p.find.matches(a,b)},dir:function(a,c,d){var e=[],f=a[c];while(f&&f.nodeType!==9&&(d===b||f.nodeType!==1||!p(f).is(d)))f.nodeType===1&&e.push(f),f=f[c];return e},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var bl="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",bm=/ jQuery\d+="(?:null|\d+)"/g,bn=/^\s+/,bo=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bp=/<([\w:]+)/,bq=/]","i"),bv=/^(?:checkbox|radio)$/,bw=/checked\s*(?:[^=]|=\s*.checked.)/i,bx=/\/(java|ecma)script/i,by=/^\s*\s*$/g,bz={option:[1,""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]},bA=bk(e),bB=bA.appendChild(e.createElement("div"));bz.optgroup=bz.option,bz.tbody=bz.tfoot=bz.colgroup=bz.caption=bz.thead,bz.th=bz.td,p.support.htmlSerialize||(bz._default=[1,"X
    ","
    "]),p.fn.extend({text:function(a){return p.access(this,function(a){return a===b?p.text(this):this.empty().append((this[0]&&this[0].ownerDocument||e).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(p.isFunction(a))return this.each(function(b){p(this).wrapAll(a.call(this,b))});if(this[0]){var b=p(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return p.isFunction(a)?this.each(function(b){p(this).wrapInner(a.call(this,b))}):this.each(function(){var b=p(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=p.isFunction(a);return this.each(function(c){p(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){p.nodeName(this,"body")||p(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(a,this.firstChild)})},before:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(a,this),"before",this.selector)}},after:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(this,a),"after",this.selector)}},remove:function(a,b){var c,d=0;for(;(c=this[d])!=null;d++)if(!a||p.filter(a,[c]).length)!b&&c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),p.cleanData([c])),c.parentNode&&c.parentNode.removeChild(c);return this},empty:function(){var a,b=0;for(;(a=this[b])!=null;b++){a.nodeType===1&&p.cleanData(a.getElementsByTagName("*"));while(a.firstChild)a.removeChild(a.firstChild)}return this},clone:function(a,b){return a=a==null?!1:a,b=b==null?a:b,this.map(function(){return p.clone(this,a,b)})},html:function(a){return p.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(bm,""):b;if(typeof a=="string"&&!bs.test(a)&&(p.support.htmlSerialize||!bu.test(a))&&(p.support.leadingWhitespace||!bn.test(a))&&!bz[(bp.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(bo,"<$1>");try{for(;d1&&typeof j=="string"&&bw.test(j))return this.each(function(){p(this).domManip(a,c,d)});if(p.isFunction(j))return this.each(function(e){var f=p(this);a[0]=j.call(this,e,c?f.html():b),f.domManip(a,c,d)});if(this[0]){e=p.buildFragment(a,this,k),g=e.fragment,f=g.firstChild,g.childNodes.length===1&&(g=f);if(f){c=c&&p.nodeName(f,"tr");for(h=e.cacheable||l-1;i0?this.clone(!0):this).get(),p(g[e])[b](d),f=f.concat(d);return this.pushStack(f,a,g.selector)}}),p.extend({clone:function(a,b,c){var d,e,f,g;p.support.html5Clone||p.isXMLDoc(a)||!bu.test("<"+a.nodeName+">")?g=a.cloneNode(!0):(bB.innerHTML=a.outerHTML,bB.removeChild(g=bB.firstChild));if((!p.support.noCloneEvent||!p.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!p.isXMLDoc(a)){bE(a,g),d=bF(a),e=bF(g);for(f=0;d[f];++f)e[f]&&bE(d[f],e[f])}if(b){bD(a,g);if(c){d=bF(a),e=bF(g);for(f=0;d[f];++f)bD(d[f],e[f])}}return d=e=null,g},clean:function(a,b,c,d){var f,g,h,i,j,k,l,m,n,o,q,r,s=b===e&&bA,t=[];if(!b||typeof b.createDocumentFragment=="undefined")b=e;for(f=0;(h=a[f])!=null;f++){typeof h=="number"&&(h+="");if(!h)continue;if(typeof h=="string")if(!br.test(h))h=b.createTextNode(h);else{s=s||bk(b),l=b.createElement("div"),s.appendChild(l),h=h.replace(bo,"<$1>"),i=(bp.exec(h)||["",""])[1].toLowerCase(),j=bz[i]||bz._default,k=j[0],l.innerHTML=j[1]+h+j[2];while(k--)l=l.lastChild;if(!p.support.tbody){m=bq.test(h),n=i==="table"&&!m?l.firstChild&&l.firstChild.childNodes:j[1]===""&&!m?l.childNodes:[];for(g=n.length-1;g>=0;--g)p.nodeName(n[g],"tbody")&&!n[g].childNodes.length&&n[g].parentNode.removeChild(n[g])}!p.support.leadingWhitespace&&bn.test(h)&&l.insertBefore(b.createTextNode(bn.exec(h)[0]),l.firstChild),h=l.childNodes,l.parentNode.removeChild(l)}h.nodeType?t.push(h):p.merge(t,h)}l&&(h=l=s=null);if(!p.support.appendChecked)for(f=0;(h=t[f])!=null;f++)p.nodeName(h,"input")?bG(h):typeof h.getElementsByTagName!="undefined"&&p.grep(h.getElementsByTagName("input"),bG);if(c){q=function(a){if(!a.type||bx.test(a.type))return d?d.push(a.parentNode?a.parentNode.removeChild(a):a):c.appendChild(a)};for(f=0;(h=t[f])!=null;f++)if(!p.nodeName(h,"script")||!q(h))c.appendChild(h),typeof h.getElementsByTagName!="undefined"&&(r=p.grep(p.merge([],h.getElementsByTagName("script")),q),t.splice.apply(t,[f+1,0].concat(r)),f+=r.length)}return t},cleanData:function(a,b){var c,d,e,f,g=0,h=p.expando,i=p.cache,j=p.support.deleteExpando,k=p.event.special;for(;(e=a[g])!=null;g++)if(b||p.acceptData(e)){d=e[h],c=d&&i[d];if(c){if(c.events)for(f in c.events)k[f]?p.event.remove(e,f):p.removeEvent(e,f,c.handle);i[d]&&(delete i[d],j?delete e[h]:e.removeAttribute?e.removeAttribute(h):e[h]=null,p.deletedIds.push(d))}}}}),function(){var a,b;p.uaMatch=function(a){a=a.toLowerCase();var b=/(chrome)[ \/]([\w.]+)/.exec(a)||/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||a.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},a=p.uaMatch(g.userAgent),b={},a.browser&&(b[a.browser]=!0,b.version=a.version),b.chrome?b.webkit=!0:b.webkit&&(b.safari=!0),p.browser=b,p.sub=function(){function a(b,c){return new a.fn.init(b,c)}p.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function c(c,d){return d&&d instanceof p&&!(d instanceof a)&&(d=a(d)),p.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(e);return a}}();var bH,bI,bJ,bK=/alpha\([^)]*\)/i,bL=/opacity=([^)]*)/,bM=/^(top|right|bottom|left)$/,bN=/^(none|table(?!-c[ea]).+)/,bO=/^margin/,bP=new RegExp("^("+q+")(.*)$","i"),bQ=new RegExp("^("+q+")(?!px)[a-z%]+$","i"),bR=new RegExp("^([-+])=("+q+")","i"),bS={},bT={position:"absolute",visibility:"hidden",display:"block"},bU={letterSpacing:0,fontWeight:400},bV=["Top","Right","Bottom","Left"],bW=["Webkit","O","Moz","ms"],bX=p.fn.toggle;p.fn.extend({css:function(a,c){return p.access(this,function(a,c,d){return d!==b?p.style(a,c,d):p.css(a,c)},a,c,arguments.length>1)},show:function(){return b$(this,!0)},hide:function(){return b$(this)},toggle:function(a,b){var c=typeof a=="boolean";return p.isFunction(a)&&p.isFunction(b)?bX.apply(this,arguments):this.each(function(){(c?a:bZ(this))?p(this).show():p(this).hide()})}}),p.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bH(a,"opacity");return c===""?"1":c}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":p.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!a||a.nodeType===3||a.nodeType===8||!a.style)return;var f,g,h,i=p.camelCase(c),j=a.style;c=p.cssProps[i]||(p.cssProps[i]=bY(j,i)),h=p.cssHooks[c]||p.cssHooks[i];if(d===b)return h&&"get"in h&&(f=h.get(a,!1,e))!==b?f:j[c];g=typeof d,g==="string"&&(f=bR.exec(d))&&(d=(f[1]+1)*f[2]+parseFloat(p.css(a,c)),g="number");if(d==null||g==="number"&&isNaN(d))return;g==="number"&&!p.cssNumber[i]&&(d+="px");if(!h||!("set"in h)||(d=h.set(a,d,e))!==b)try{j[c]=d}catch(k){}},css:function(a,c,d,e){var f,g,h,i=p.camelCase(c);return c=p.cssProps[i]||(p.cssProps[i]=bY(a.style,i)),h=p.cssHooks[c]||p.cssHooks[i],h&&"get"in h&&(f=h.get(a,!0,e)),f===b&&(f=bH(a,c)),f==="normal"&&c in bU&&(f=bU[c]),d||e!==b?(g=parseFloat(f),d||p.isNumeric(g)?g||0:f):f},swap:function(a,b,c){var d,e,f={};for(e in b)f[e]=a.style[e],a.style[e]=b[e];d=c.call(a);for(e in b)a.style[e]=f[e];return d}}),a.getComputedStyle?bH=function(b,c){var d,e,f,g,h=a.getComputedStyle(b,null),i=b.style;return h&&(d=h[c],d===""&&!p.contains(b.ownerDocument,b)&&(d=p.style(b,c)),bQ.test(d)&&bO.test(c)&&(e=i.width,f=i.minWidth,g=i.maxWidth,i.minWidth=i.maxWidth=i.width=d,d=h.width,i.width=e,i.minWidth=f,i.maxWidth=g)),d}:e.documentElement.currentStyle&&(bH=function(a,b){var c,d,e=a.currentStyle&&a.currentStyle[b],f=a.style;return e==null&&f&&f[b]&&(e=f[b]),bQ.test(e)&&!bM.test(b)&&(c=f.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":e,e=f.pixelLeft+"px",f.left=c,d&&(a.runtimeStyle.left=d)),e===""?"auto":e}),p.each(["height","width"],function(a,b){p.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth===0&&bN.test(bH(a,"display"))?p.swap(a,bT,function(){return cb(a,b,d)}):cb(a,b,d)},set:function(a,c,d){return b_(a,c,d?ca(a,b,d,p.support.boxSizing&&p.css(a,"boxSizing")==="border-box"):0)}}}),p.support.opacity||(p.cssHooks.opacity={get:function(a,b){return bL.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=p.isNumeric(b)?"alpha(opacity="+b*100+")":"",f=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&p.trim(f.replace(bK,""))===""&&c.removeAttribute){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bK.test(f)?f.replace(bK,e):f+" "+e}}),p(function(){p.support.reliableMarginRight||(p.cssHooks.marginRight={get:function(a,b){return p.swap(a,{display:"inline-block"},function(){if(b)return bH(a,"marginRight")})}}),!p.support.pixelPosition&&p.fn.position&&p.each(["top","left"],function(a,b){p.cssHooks[b]={get:function(a,c){if(c){var d=bH(a,b);return bQ.test(d)?p(a).position()[b]+"px":d}}}})}),p.expr&&p.expr.filters&&(p.expr.filters.hidden=function(a){return a.offsetWidth===0&&a.offsetHeight===0||!p.support.reliableHiddenOffsets&&(a.style&&a.style.display||bH(a,"display"))==="none"},p.expr.filters.visible=function(a){return!p.expr.filters.hidden(a)}),p.each({margin:"",padding:"",border:"Width"},function(a,b){p.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bV[d]+b]=e[d]||e[d-2]||e[0];return f}},bO.test(a)||(p.cssHooks[a+b].set=b_)});var cd=/%20/g,ce=/\[\]$/,cf=/\r?\n/g,cg=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ch=/^(?:select|textarea)/i;p.fn.extend({serialize:function(){return p.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?p.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ch.test(this.nodeName)||cg.test(this.type))}).map(function(a,b){var c=p(this).val();return c==null?null:p.isArray(c)?p.map(c,function(a,c){return{name:b.name,value:a.replace(cf,"\r\n")}}):{name:b.name,value:c.replace(cf,"\r\n")}}).get()}}),p.param=function(a,c){var d,e=[],f=function(a,b){b=p.isFunction(b)?b():b==null?"":b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=p.ajaxSettings&&p.ajaxSettings.traditional);if(p.isArray(a)||a.jquery&&!p.isPlainObject(a))p.each(a,function(){f(this.name,this.value)});else for(d in a)ci(d,a[d],c,f);return e.join("&").replace(cd,"+")};var cj,ck,cl=/#.*$/,cm=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,cn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,co=/^(?:GET|HEAD)$/,cp=/^\/\//,cq=/\?/,cr=/)<[^<]*)*<\/script>/gi,cs=/([?&])_=[^&]*/,ct=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,cu=p.fn.load,cv={},cw={},cx=["*/"]+["*"];try{ck=f.href}catch(cy){ck=e.createElement("a"),ck.href="",ck=ck.href}cj=ct.exec(ck.toLowerCase())||[],p.fn.load=function(a,c,d){if(typeof a!="string"&&cu)return cu.apply(this,arguments);if(!this.length)return this;var e,f,g,h=this,i=a.indexOf(" ");return i>=0&&(e=a.slice(i,a.length),a=a.slice(0,i)),p.isFunction(c)?(d=c,c=b):c&&typeof c=="object"&&(f="POST"),p.ajax({url:a,type:f,dataType:"html",data:c,complete:function(a,b){d&&h.each(d,g||[a.responseText,b,a])}}).done(function(a){g=arguments,h.html(e?p("
    ").append(a.replace(cr,"")).find(e):a)}),this},p.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){p.fn[b]=function(a){return this.on(b,a)}}),p.each(["get","post"],function(a,c){p[c]=function(a,d,e,f){return p.isFunction(d)&&(f=f||e,e=d,d=b),p.ajax({type:c,url:a,data:d,success:e,dataType:f})}}),p.extend({getScript:function(a,c){return p.get(a,b,c,"script")},getJSON:function(a,b,c){return p.get(a,b,c,"json")},ajaxSetup:function(a,b){return b?cB(a,p.ajaxSettings):(b=a,a=p.ajaxSettings),cB(a,b),a},ajaxSettings:{url:ck,isLocal:cn.test(cj[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":cx},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":p.parseJSON,"text xml":p.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:cz(cv),ajaxTransport:cz(cw),ajax:function(a,c){function y(a,c,f,i){var k,s,t,u,w,y=c;if(v===2)return;v=2,h&&clearTimeout(h),g=b,e=i||"",x.readyState=a>0?4:0,f&&(u=cC(l,x,f));if(a>=200&&a<300||a===304)l.ifModified&&(w=x.getResponseHeader("Last-Modified"),w&&(p.lastModified[d]=w),w=x.getResponseHeader("Etag"),w&&(p.etag[d]=w)),a===304?(y="notmodified",k=!0):(k=cD(l,u),y=k.state,s=k.data,t=k.error,k=!t);else{t=y;if(!y||a)y="error",a<0&&(a=0)}x.status=a,x.statusText=(c||y)+"",k?o.resolveWith(m,[s,y,x]):o.rejectWith(m,[x,y,t]),x.statusCode(r),r=b,j&&n.trigger("ajax"+(k?"Success":"Error"),[x,l,k?s:t]),q.fireWith(m,[x,y]),j&&(n.trigger("ajaxComplete",[x,l]),--p.active||p.event.trigger("ajaxStop"))}typeof a=="object"&&(c=a,a=b),c=c||{};var d,e,f,g,h,i,j,k,l=p.ajaxSetup({},c),m=l.context||l,n=m!==l&&(m.nodeType||m instanceof p)?p(m):p.event,o=p.Deferred(),q=p.Callbacks("once memory"),r=l.statusCode||{},t={},u={},v=0,w="canceled",x={readyState:0,setRequestHeader:function(a,b){if(!v){var c=a.toLowerCase();a=u[c]=u[c]||a,t[a]=b}return this},getAllResponseHeaders:function(){return v===2?e:null},getResponseHeader:function(a){var c;if(v===2){if(!f){f={};while(c=cm.exec(e))f[c[1].toLowerCase()]=c[2]}c=f[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){return v||(l.mimeType=a),this},abort:function(a){return a=a||w,g&&g.abort(a),y(0,a),this}};o.promise(x),x.success=x.done,x.error=x.fail,x.complete=q.add,x.statusCode=function(a){if(a){var b;if(v<2)for(b in a)r[b]=[r[b],a[b]];else b=a[x.status],x.always(b)}return this},l.url=((a||l.url)+"").replace(cl,"").replace(cp,cj[1]+"//"),l.dataTypes=p.trim(l.dataType||"*").toLowerCase().split(s),l.crossDomain==null&&(i=ct.exec(l.url.toLowerCase())||!1,l.crossDomain=i&&i.join(":")+(i[3]?"":i[1]==="http:"?80:443)!==cj.join(":")+(cj[3]?"":cj[1]==="http:"?80:443)),l.data&&l.processData&&typeof l.data!="string"&&(l.data=p.param(l.data,l.traditional)),cA(cv,l,c,x);if(v===2)return x;j=l.global,l.type=l.type.toUpperCase(),l.hasContent=!co.test(l.type),j&&p.active++===0&&p.event.trigger("ajaxStart");if(!l.hasContent){l.data&&(l.url+=(cq.test(l.url)?"&":"?")+l.data,delete l.data),d=l.url;if(l.cache===!1){var z=p.now(),A=l.url.replace(cs,"$1_="+z);l.url=A+(A===l.url?(cq.test(l.url)?"&":"?")+"_="+z:"")}}(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&x.setRequestHeader("Content-Type",l.contentType),l.ifModified&&(d=d||l.url,p.lastModified[d]&&x.setRequestHeader("If-Modified-Since",p.lastModified[d]),p.etag[d]&&x.setRequestHeader("If-None-Match",p.etag[d])),x.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+(l.dataTypes[0]!=="*"?", "+cx+"; q=0.01":""):l.accepts["*"]);for(k in l.headers)x.setRequestHeader(k,l.headers[k]);if(!l.beforeSend||l.beforeSend.call(m,x,l)!==!1&&v!==2){w="abort";for(k in{success:1,error:1,complete:1})x[k](l[k]);g=cA(cw,l,c,x);if(!g)y(-1,"No Transport");else{x.readyState=1,j&&n.trigger("ajaxSend",[x,l]),l.async&&l.timeout>0&&(h=setTimeout(function(){x.abort("timeout")},l.timeout));try{v=1,g.send(t,y)}catch(B){if(v<2)y(-1,B);else throw B}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var cE=[],cF=/\?/,cG=/(=)\?(?=&|$)|\?\?/,cH=p.now();p.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=cE.pop()||p.expando+"_"+cH++;return this[a]=!0,a}}),p.ajaxPrefilter("json jsonp",function(c,d,e){var f,g,h,i=c.data,j=c.url,k=c.jsonp!==!1,l=k&&cG.test(j),m=k&&!l&&typeof i=="string"&&!(c.contentType||"").indexOf("application/x-www-form-urlencoded")&&cG.test(i);if(c.dataTypes[0]==="jsonp"||l||m)return f=c.jsonpCallback=p.isFunction(c.jsonpCallback)?c.jsonpCallback():c.jsonpCallback,g=a[f],l?c.url=j.replace(cG,"$1"+f):m?c.data=i.replace(cG,"$1"+f):k&&(c.url+=(cF.test(j)?"&":"?")+c.jsonp+"="+f),c.converters["script json"]=function(){return h||p.error(f+" was not called"),h[0]},c.dataTypes[0]="json",a[f]=function(){h=arguments},e.always(function(){a[f]=g,c[f]&&(c.jsonpCallback=d.jsonpCallback,cE.push(f)),h&&p.isFunction(g)&&g(h[0]),h=g=b}),"script"}),p.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){return p.globalEval(a),a}}}),p.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),p.ajaxTransport("script",function(a){if(a.crossDomain){var c,d=e.head||e.getElementsByTagName("head")[0]||e.documentElement;return{send:function(f,g){c=e.createElement("script"),c.async="async",a.scriptCharset&&(c.charset=a.scriptCharset),c.src=a.url,c.onload=c.onreadystatechange=function(a,e){if(e||!c.readyState||/loaded|complete/.test(c.readyState))c.onload=c.onreadystatechange=null,d&&c.parentNode&&d.removeChild(c),c=b,e||g(200,"success")},d.insertBefore(c,d.firstChild)},abort:function(){c&&c.onload(0,1)}}}});var cI,cJ=a.ActiveXObject?function(){for(var a in cI)cI[a](0,1)}:!1,cK=0;p.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cL()||cM()}:cL,function(a){p.extend(p.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(p.ajaxSettings.xhr()),p.support.ajax&&p.ajaxTransport(function(c){if(!c.crossDomain||p.support.cors){var d;return{send:function(e,f){var g,h,i=c.xhr();c.username?i.open(c.type,c.url,c.async,c.username,c.password):i.open(c.type,c.url,c.async);if(c.xhrFields)for(h in c.xhrFields)i[h]=c.xhrFields[h];c.mimeType&&i.overrideMimeType&&i.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(h in e)i.setRequestHeader(h,e[h])}catch(j){}i.send(c.hasContent&&c.data||null),d=function(a,e){var h,j,k,l,m;try{if(d&&(e||i.readyState===4)){d=b,g&&(i.onreadystatechange=p.noop,cJ&&delete cI[g]);if(e)i.readyState!==4&&i.abort();else{h=i.status,k=i.getAllResponseHeaders(),l={},m=i.responseXML,m&&m.documentElement&&(l.xml=m);try{l.text=i.responseText}catch(a){}try{j=i.statusText}catch(n){j=""}!h&&c.isLocal&&!c.crossDomain?h=l.text?200:404:h===1223&&(h=204)}}}catch(o){e||f(-1,o)}l&&f(h,j,l,k)},c.async?i.readyState===4?setTimeout(d,0):(g=++cK,cJ&&(cI||(cI={},p(a).unload(cJ)),cI[g]=d),i.onreadystatechange=d):d()},abort:function(){d&&d(0,1)}}}});var cN,cO,cP=/^(?:toggle|show|hide)$/,cQ=new RegExp("^(?:([-+])=|)("+q+")([a-z%]*)$","i"),cR=/queueHooks$/,cS=[cY],cT={"*":[function(a,b){var c,d,e=this.createTween(a,b),f=cQ.exec(b),g=e.cur(),h=+g||0,i=1,j=20;if(f){c=+f[2],d=f[3]||(p.cssNumber[a]?"":"px");if(d!=="px"&&h){h=p.css(e.elem,a,!0)||c||1;do i=i||".5",h=h/i,p.style(e.elem,a,h+d);while(i!==(i=e.cur()/g)&&i!==1&&--j)}e.unit=d,e.start=h,e.end=f[1]?h+(f[1]+1)*c:c}return e}]};p.Animation=p.extend(cW,{tweener:function(a,b){p.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");var c,d=0,e=a.length;for(;d-1,j={},k={},l,m;i?(k=e.position(),l=k.top,m=k.left):(l=parseFloat(g)||0,m=parseFloat(h)||0),p.isFunction(b)&&(b=b.call(a,c,f)),b.top!=null&&(j.top=b.top-f.top+l),b.left!=null&&(j.left=b.left-f.left+m),"using"in b?b.using.call(a,j):e.css(j)}},p.fn.extend({position:function(){if(!this[0])return;var a=this[0],b=this.offsetParent(),c=this.offset(),d=c_.test(b[0].nodeName)?{top:0,left:0}:b.offset();return c.top-=parseFloat(p.css(a,"marginTop"))||0,c.left-=parseFloat(p.css(a,"marginLeft"))||0,d.top+=parseFloat(p.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(p.css(b[0],"borderLeftWidth"))||0,{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||e.body;while(a&&!c_.test(a.nodeName)&&p.css(a,"position")==="static")a=a.offsetParent;return a||e.body})}}),p.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);p.fn[a]=function(e){return p.access(this,function(a,e,f){var g=da(a);if(f===b)return g?c in g?g[c]:g.document.documentElement[e]:a[e];g?g.scrollTo(d?p(g).scrollLeft():f,d?f:p(g).scrollTop()):a[e]=f},a,e,arguments.length,null)}}),p.each({Height:"height",Width:"width"},function(a,c){p.each({padding:"inner"+a,content:c,"":"outer"+a},function(d,e){p.fn[e]=function(e,f){var g=arguments.length&&(d||typeof e!="boolean"),h=d||(e===!0||f===!0?"margin":"border");return p.access(this,function(c,d,e){var f;return p.isWindow(c)?c.document.documentElement["client"+a]:c.nodeType===9?(f=c.documentElement,Math.max(c.body["scroll"+a],f["scroll"+a],c.body["offset"+a],f["offset"+a],f["client"+a])):e===b?p.css(c,d,e,h):p.style(c,d,e,h)},c,g?e:b,g,null)}})}),a.jQuery=a.$=p,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return p})})(window); \ No newline at end of file -- cgit v1.2.3-54-g00ecf From 05f309d7e57a66d9309abbf19b4328bad514b978 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 24 Sep 2012 21:13:02 -0500 Subject: Add a new column to developer repo stats Signed-off-by: Dan McGee --- devel/views.py | 5 +++++ templates/devel/index.html | 3 +++ 2 files changed, 8 insertions(+) diff --git a/devel/views.py b/devel/views.py index 23ff9f74..ea85a901 100644 --- a/devel/views.py +++ b/devel/views.py @@ -55,6 +55,11 @@ def index(request): total_ct=Count('packages'), flagged_ct=Count('packages__flag_date')) repos = Repo.objects.all().annotate( total_ct=Count('packages'), flagged_ct=Count('packages__flag_date')) + # the join is huge unless we do this separately, so merge the result here + repo_maintainers = dict(Repo.objects.all().values_list('id').annotate( + Count('userprofile'))) + for repo in repos: + repo.maintainer_ct = repo_maintainers.get(repo.id, 0) maintainers = get_annotated_maintainers() diff --git a/templates/devel/index.html b/templates/devel/index.html index 488b6755..a07a4190 100644 --- a/templates/devel/index.html +++ b/templates/devel/index.html @@ -216,6 +216,7 @@

    Stats by Repository

    + @@ -228,6 +229,8 @@

    Stats by Repository

    + + {% endfor %} -- cgit v1.2.3-54-g00ecf From a1c4d831c92cbb32cb8c34f95b8cd5eb541cdf00 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 24 Sep 2012 21:23:18 -0500 Subject: Exclude inactive developers in maintainer count Signed-off-by: Dan McGee --- devel/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devel/views.py b/devel/views.py index ea85a901..083665d9 100644 --- a/devel/views.py +++ b/devel/views.py @@ -56,7 +56,8 @@ def index(request): repos = Repo.objects.all().annotate( total_ct=Count('packages'), flagged_ct=Count('packages__flag_date')) # the join is huge unless we do this separately, so merge the result here - repo_maintainers = dict(Repo.objects.all().values_list('id').annotate( + repo_maintainers = dict(Repo.objects.order_by().filter( + userprofile__user__is_active=True).values_list('id').annotate( Count('userprofile'))) for repo in repos: repo.maintainer_ct = repo_maintainers.get(repo.id, 0) -- cgit v1.2.3-54-g00ecf From 3eed426027ed6bc87b58f82d48da06bea55b265f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 25 Sep 2012 00:30:05 -0500 Subject: Add structured data to developer listing pages Signed-off-by: Dan McGee --- public/views.py | 3 +++ templates/public/developer_list.html | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/public/views.py b/public/views.py index 312cb3b2..35315e0e 100644 --- a/public/views.py +++ b/public/views.py @@ -29,14 +29,17 @@ def index(request): USER_LISTS = { 'devs': { 'user_type': 'Developers', + 'user_title': 'Developer', 'description': "This is a list of the current Arch Linux Developers. They maintain the [core] and [extra] package repositories in addition to doing any other developer duties.", }, 'tus': { 'user_type': 'Trusted Users', + 'user_title': 'Trusted User', 'description': "Here are all your friendly Arch Linux Trusted Users who are in charge of the [community] repository.", }, 'fellows': { 'user_type': 'Fellows', + 'user_title': 'Fellow', 'description': "Below you can find a list of ex-developers (aka project fellows). These folks helped make Arch what it is today. Thanks!", }, } diff --git a/templates/public/developer_list.html b/templates/public/developer_list.html index 376ab433..df4137eb 100644 --- a/templates/public/developer_list.html +++ b/templates/public/developer_list.html @@ -12,17 +12,24 @@
    Repository # Packages # Flagged# Maintainers
    {{ repo.flagged_ct }} packages{{ repo.maintainer_ct }} maintainers
    {% for dev in dev_list %} {% with dev.userprofile as prof %} - + - + {% if user.is_authenticated %} -- cgit v1.2.3-54-g00ecf From cd625181344c73bdb761520dd23549a112549501 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 13 Nov 2012 09:12:09 -0600 Subject: Slight master key template adjustment This is in anticipation of moving the visualization stuff to this page rather than grouped with unrelated things. Signed-off-by: Dan McGee --- templates/public/keys.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/templates/public/keys.html b/templates/public/keys.html index 1b027202..f23d1c40 100644 --- a/templates/public/keys.html +++ b/templates/public/keys.html @@ -47,6 +47,10 @@

    Master Signing Keys

    {% endfor %}
    - Image for {{ prof.alias }} + Image for {{ prof.alias }} + + + + +
    + +

    {{ dev.get_full_name }}{% if prof.latin_name %} ({{ prof.latin_name}}){% endif %}

    - + @@ -38,7 +45,7 @@

    {{ dev.get_full_name }}{% if prof.latin_name %} ({{ prof.latin_name}}){% end

    - @@ -46,7 +53,7 @@

    {{ dev.get_full_name }}{% if prof.latin_name %} ({{ prof.latin_name}}){% end

    - + -- cgit v1.2.3-54-g00ecf From 67fef06fe8d07e8b832e447a6b2064fb051a5ef9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 25 Sep 2012 00:49:54 -0500 Subject: Only show staging feeds to logged-in users This doesn't prevent unauthenticated users from accessing the feeds, but it should reduce clutter and confusion on the feeds index page for users unlikely to need these feeds. Signed-off-by: Dan McGee --- public/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/views.py b/public/views.py index 35315e0e..42f2f345 100644 --- a/public/views.py +++ b/public/views.py @@ -86,9 +86,12 @@ def download(request): @cache_control(max_age=300) def feeds(request): + repos = Repo.objects.all() + if not request.user.is_authenticated(): + repos = repos.filter(staging=False) context = { 'arches': Arch.objects.all(), - 'repos': Repo.objects.all(), + 'repos': repos, } return render(request, 'public/feeds.html', context) -- cgit v1.2.3-54-g00ecf From 182a45ace9ba4690aa826b3faf34884fbd3f68ae Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 25 Sep 2012 18:20:50 -0500 Subject: Use minified typeahead JS file Signed-off-by: Dan McGee --- templates/public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/public/index.html b/templates/public/index.html index 4af5995e..3aab5da2 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -217,7 +217,7 @@

    More Resources

    {% endcache %} {% load cdn %}{% jquery %} - + {% endblock %} diff --git a/templates/packages/details_relatedto.html b/templates/packages/details_relatedto.html index 1ffe2884..e14375d3 100644 --- a/templates/packages/details_relatedto.html +++ b/templates/packages/details_relatedto.html @@ -1,10 +1,2 @@ -{% load package_extras %} -{% for related in all_related %} -{% with related.get_best_satisfier as best_satisfier %} -{% ifequal best_satisfier None %} -{{ related.name }}{{ related.comparison|default:"" }}{{ related.version|default:"" }}{% if not forloop.last %}, {% endif %} -{% else %} -{% pkg_details_link best_satisfier %}{{ related.comparison|default:"" }}{{related.version|default:"" }}{% if not forloop.last %}, {% endif %} -{% endifequal %} -{% endwith %} -{% endfor %} +{% load package_extras %}{% for related in all_related %}{% with related.get_best_satisfier as best_satisfier %}{% ifequal best_satisfier None %}{{ related.name }}{% else %}{% pkg_details_link best_satisfier %}{% endifequal %}{{ related.comparison|default:"" }}{{ related.version|default:"" }}{% if not forloop.last %}, {% endif %} +{% endwith %}{% endfor %} -- cgit v1.2.3-54-g00ecf From 3e076596220774dcf5e32325b7d387e581e3df28 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 2 Oct 2012 06:39:55 -0500 Subject: Add a few more redirects for old links This covers more .php pages noticed in Google webmaster tools, as well as some links to former documentation. Signed-off-by: Dan McGee --- urls.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/urls.py b/urls.py index f01aeecb..fd5ff7cf 100644 --- a/urls.py +++ b/urls.py @@ -98,11 +98,21 @@ legacy_urls = ( ('^about.php', '/about/'), ('^changelog.php', '/packages/?sort=-last_update'), + ('^devs.php', '/developers/'), + ('^donations.php', '/donate/'), ('^download.php', '/download/'), ('^index.php', '/'), ('^logos.php', '/art/'), ('^news.php', '/news/'), ('^packages.php', '/packages/'), + ('^people.php', '/developers/'), + + ('^docs/en/guide/install/arch-install-guide.html', + 'https://wiki.archlinux.org/index.php/Installation_Guide'), + ('^docs/en/', + 'https://wiki.archlinux.org/'), + ('^docs/', + 'https://wiki.archlinux.org/'), ) urlpatterns += [url(old_url, RedirectView.as_view(url=new_url)) -- cgit v1.2.3-54-g00ecf From 1decbc079ff8ab9798cef0ca02310357f8f4ba0c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 2 Oct 2012 08:48:54 -0500 Subject: JSLint suggested script cleanups Signed-off-by: Dan McGee --- sitestatic/archweb.js | 51 ++++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/sitestatic/archweb.js b/sitestatic/archweb.js index a42e0208..d49665a4 100644 --- a/sitestatic/archweb.js +++ b/sitestatic/archweb.js @@ -1,6 +1,7 @@ +/*'use strict';*/ /* tablesorter custom parsers for various pages: * devel/index.html, mirrors/status.html, todolists/view.html */ -if (typeof $.tablesorter !== 'undefined') { +if (typeof $ !== 'undefined' && typeof $.tablesorter !== 'undefined') { $.tablesorter.addParser({ id: 'pkgcount', is: function(s) { return false; }, @@ -60,7 +61,7 @@ if (typeof $.tablesorter !== 'undefined') { format: function(s, t, c) { /* TODO: this assumes our magic class is the only one */ var epoch = $(c).attr('class'); - if (!epoch.indexOf('epoch-') == 0) { + if (epoch.indexOf('epoch-') !== 0) { return 0; } return epoch.slice(6); @@ -101,8 +102,8 @@ if (typeof $.tablesorter !== 'undefined') { if (!matches) { return 0; } - var size = parseFloat(matches[1]); - var suffix = matches[2]; + var size = parseFloat(matches[1]), + suffix = matches[2]; switch(suffix) { /* intentional fall-through at each level */ @@ -139,13 +140,13 @@ if (typeof $.tablesorter !== 'undefined') { (function($) { $.fn.enableCheckboxRangeSelection = function() { - var lastCheckbox = null; - var lastElement = null; + var lastCheckbox = null, + lastElement = null, + spec = this; - var spec = this; spec.unbind("click.checkboxrange"); spec.bind("click.checkboxrange", function(e) { - if (lastCheckbox != null && e.shiftKey) { + if (lastCheckbox !== null && e.shiftKey) { spec.slice( Math.min(spec.index(lastCheckbox), spec.index(e.target)), Math.max(spec.index(lastCheckbox), spec.index(e.target)) + 1 @@ -190,7 +191,7 @@ function ajaxifyFiles() { } if (list_items.length > 0) { $('#pkgfilelist').append('
      ' + list_items.join('') + '
    '); - } else if (data.files_last_update == null) { + } else if (data.files_last_update === null) { $('#pkgfilelist').append('

    No file list available.

    '); } else { $('#pkgfilelist').append('

    Package has no files.

    '); @@ -200,12 +201,12 @@ function ajaxifyFiles() { } function collapseDependsList(list) { - var limit = 20; list = $(list); // Hide everything past a given limit. Don't do anything if we don't have // enough items, or the link already exists. - var linkid = list.attr('id') + 'link'; - var items = list.find('li').slice(limit); + var limit = 20, + linkid = list.attr('id') + 'link', + items = list.find('li').slice(limit); if (items.length <= 1 || $('#' + linkid).length > 0) { return; } @@ -246,8 +247,8 @@ function collapseRelatedTo(elements) { /* packages/differences.html */ function filter_packages() { /* start with all rows, and then remove ones we shouldn't show */ - var rows = $('#tbody_differences').children(); - var all_rows = rows; + var rows = $('#tbody_differences').children(), + all_rows = rows; if (!$('#id_multilib').is(':checked')) { rows = rows.not('.multilib').not('.multilib-testing'); } @@ -300,6 +301,7 @@ function filter_packages_reset() { /* todolists/view.html */ function todolist_flag() { + // TODO: fix usage of this var link = this; $.getJSON(link.href, function(data) { if (data.complete) { @@ -317,8 +319,8 @@ function todolist_flag() { function filter_pkgs_list(filter_ele, tbody_ele) { /* start with all rows, and then remove ones we shouldn't show */ - var rows = $(tbody_ele).children(); - var all_rows = rows; + var rows = $(tbody_ele).children(), + all_rows = rows; /* apply the filters, cheaper ones first */ if ($('#id_mine_only').is(':checked')) { rows = rows.filter('.mine'); @@ -351,15 +353,16 @@ function filter_pkgs_reset(callback) { /* signoffs.html */ function signoff_package() { + // TODO: fix usage of this var link = this; $.getJSON(link.href, function(data) { link = $(link); - var signoff = null; - var cell = link.closest('td'); + var signoff = null, + cell = link.closest('td'); if (data.created) { signoff = $('
  • ').addClass('signed-username').text(data.user); var list = cell.children('ul.signoff-list'); - if (list.size() == 0) { + if (list.size() === 0) { list = $('
      ').prependTo(cell); } list.append(signoff); @@ -406,16 +409,14 @@ function signoff_package() { function filter_signoffs() { /* start with all rows, and then remove ones we shouldn't show */ - var rows = $('#tbody_signoffs').children(); - var all_rows = rows; + var rows = $('#tbody_signoffs').children(), + all_rows = rows; /* apply arch and repo filters */ $('#signoffs_filter .arch_filter').add( '#signoffs_filter .repo_filter').each(function() { if (!$(this).is(':checked')) { rows = rows.not('.' + $(this).val()); } - }); - /* and then the slightly more expensive pending check */ if ($('#id_pending').is(':checked')) { rows = rows.has('td.signoff-no'); } @@ -436,8 +437,8 @@ function filter_signoffs_reset() { /* visualizations */ function format_filesize(size, decimals) { /*var labels = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];*/ - var labels = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - var label = 0; + var labels = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], + label = 0; while (size > 2048.0 && label < labels.length - 1) { label++; -- cgit v1.2.3-54-g00ecf From 8463c3c121c2a7afcbcca08b9b1ef6b5d1782587 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 4 Oct 2012 13:32:54 -0500 Subject: Update bugs link for feeds for all projects Apparantly 'project=99' is not the correct way to do this; 'project=0' is. Flip the links so they all use the new form. FS#31561. Signed-off-by: Dan McGee --- templates/public/feeds.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/public/feeds.html b/templates/public/feeds.html index f5378afb..4a24b8d5 100644 --- a/templates/public/feeds.html +++ b/templates/public/feeds.html @@ -82,9 +82,9 @@

      Development Feeds

  • - - - + + + -- cgit v1.2.3-54-g00ecf From 627a3567271122c7a9e71b692538863aef69fda5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 6 Oct 2012 10:34:29 -0500 Subject: Update download page for new release Signed-off-by: Dan McGee --- templates/public/download.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/public/download.html b/templates/public/download.html index 63f6d79c..4b94e183 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -7,7 +7,7 @@ {% block navbarclass %}anb-download{% endblock %} {% block content %} -{% with version="2012.09.07" kernel_version="3.5.3" %} +{% with version="2012.10.06" kernel_version="3.5.5" %}

    Arch Linux Downloads

    -- cgit v1.2.3-54-g00ecf From ddb7f4825f8bf70142735a5ba2f7729ffe5d27c1 Mon Sep 17 00:00:00 2001 From: Evangelos Foutras Date: Sat, 6 Oct 2012 23:11:15 +0300 Subject: archweb.js: Fix syntax error in filter_signoffs() Commit 1decbc079ff8ab9798cef0ca02310357f8f4ba0c "JSLint suggested script cleanups" mistakenly removed the closing brace and parenthesis of a jQuery .each() call, along with a following comment. This commit brings back the two removed lines. Signed-off-by: Evangelos Foutras Signed-off-by: Dan McGee --- sitestatic/archweb.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sitestatic/archweb.js b/sitestatic/archweb.js index d49665a4..a0c5713a 100644 --- a/sitestatic/archweb.js +++ b/sitestatic/archweb.js @@ -417,6 +417,8 @@ function filter_signoffs() { if (!$(this).is(':checked')) { rows = rows.not('.' + $(this).val()); } + }); + /* and then the slightly more expensive pending check */ if ($('#id_pending').is(':checked')) { rows = rows.has('td.signoff-no'); } -- cgit v1.2.3-54-g00ecf From f0b7e73de61c03a5018ed352605e6329611448d2 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 10 Oct 2012 20:17:55 -0500 Subject: Make mirror log time query a bit more efficient We don't need the full mirror log objects; we just need a very small subset of values from them here to do the required math and object building. Signed-off-by: Dan McGee --- mirrors/utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mirrors/utils.py b/mirrors/utils.py index bf030d39..0a32b766 100644 --- a/mirrors/utils.py +++ b/mirrors/utils.py @@ -50,12 +50,14 @@ def get_mirror_statuses(cutoff=DEFAULT_CUTOFF): # The Django ORM makes it really hard to get actual average delay in the # above query, so run a seperate query for it and we will process the # results here. - times = MirrorLog.objects.filter(is_success=True, last_sync__isnull=False, + times = MirrorLog.objects.values_list( + 'url_id', 'check_time', 'last_sync').filter( + is_success=True, last_sync__isnull=False, check_time__gte=cutoff_time) delays = {} - for log in times: - delay = log.check_time - log.last_sync - delays.setdefault(log.url_id, []).append(delay) + for url_id, check_time, last_sync in times: + delay = check_time - last_sync + delays.setdefault(url_id, []).append(delay) if urls: last_check = max([u.last_check for u in urls]) -- cgit v1.2.3-54-g00ecf From a71aa2e354599950f4bd464f0f19215f1c581141 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 12 Oct 2012 11:34:49 -0500 Subject: Make wrong permissions query more efficient This removes the subplan and per-row query in favor of a LEFT JOIN where we look for non-matching rows. Tested in sqlite3 and PostgreSQL. Signed-off-by: Dan McGee --- packages/utils.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/utils.py b/packages/utils.py index ee1b56b3..c29e2297 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -228,12 +228,13 @@ def get_wrong_permissions(): FROM packages p JOIN packages_packagerelation pr ON p.pkgbase = pr.pkgbase WHERE pr.type = %s - ) pkgs - WHERE pkgs.repo_id NOT IN ( - SELECT repo_id FROM user_profiles_allowed_repos ar + ) mp + LEFT JOIN ( + SELECT user_id, repo_id FROM user_profiles_allowed_repos ar INNER JOIN user_profiles up ON ar.userprofile_id = up.id - WHERE up.user_id = pkgs.user_id - ) + ) ur + ON mp.user_id = ur.user_id AND mp.repo_id = ur.repo_id + WHERE ur.user_id IS NULL; """ cursor = connection.cursor() cursor.execute(sql, [PackageRelation.MAINTAINER]) -- cgit v1.2.3-54-g00ecf From 5228cb5f584f076e547e1d0af695c08975801d2f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 12 Oct 2012 11:39:16 -0500 Subject: reporead: don't print full backtrace if unnecessary In the architecture agnostic case, this error is much more likely to happen, so printing it like an error message is deceiving. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index ce5c8cb7..a1e77b49 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -393,9 +393,12 @@ def db_update(archname, reponame, pkgs, force=False): populate_pkg(dbpkg, pkg, timestamp=now()) Update.objects.log_update(None, dbpkg) except IntegrityError: - logger.warning("Could not add package %s; " - "not fatal if another thread beat us to it.", - pkg.name, exc_info=True) + if architecture.agnostic: + logger.warning("Could not add package %s; " + "not fatal if another thread beat us to it.", + pkg.name) + else: + logger.exception("Could not add package %s", pkg.name) # packages in database and not in syncdb (remove from database) for pkgname in (dbset - syncset): -- cgit v1.2.3-54-g00ecf From 0b3aa29cb63c6ca07f066a4a68fa3df9b92f6216 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 14 Oct 2012 15:42:15 -0500 Subject: Refactor signoff-grabbing queries Make them a bit more efficient by adding an explicit condition on both the packages and signoff table for the repo ID, and move the common code into a shared function both can use. Signed-off-by: Dan McGee --- packages/utils.py | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/utils.py b/packages/utils.py index c29e2297..051fed8e 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -365,7 +365,8 @@ def __unicode__(self): self.pkgbase, self.version, self.arch, len(self.signoffs)) -_SQL_SPEC_OR_SIGNOFF = """ +def signoffs_id_query(model, repos): + sql = """ SELECT DISTINCT s.id FROM %s s JOIN packages p ON ( @@ -377,34 +378,29 @@ def __unicode__(self): AND s.repo_id = p.repo_id ) WHERE p.repo_id IN (%s) -""" - - -def get_current_signoffs(repos): - '''Returns a mapping of pkgbase -> signoff objects for the given repos.''' + AND s.repo_id IN (%s) + """ cursor = connection.cursor() # query pre-process- fill in table name and placeholders for IN - sql = _SQL_SPEC_OR_SIGNOFF % ('packages_signoff', - ','.join(['%s' for r in repos])) - cursor.execute(sql, [r.pk for r in repos]) + repo_sql = ','.join(['%s' for r in repos]) + sql = sql % (model._meta.db_table, repo_sql, repo_sql) + repo_ids = [r.pk for r in repos] + # repo_ids are needed twice, so double the array + cursor.execute(sql, repo_ids * 2) results = cursor.fetchall() - # fetch all of the returned signoffs by ID - to_fetch = [row[0] for row in results] - signoffs = Signoff.objects.select_related('user').in_bulk(to_fetch) - return signoffs.values() + return [row[0] for row in results] -def get_current_specifications(repos): - '''Returns a mapping of pkgbase -> signoff specification objects for the - given repos.''' - cursor = connection.cursor() - sql = _SQL_SPEC_OR_SIGNOFF % ('packages_signoffspecification', - ','.join(['%s' for r in repos])) - cursor.execute(sql, [r.pk for r in repos]) +def get_current_signoffs(repos): + '''Returns a list of signoff objects for the given repos.''' + to_fetch = signoffs_id_query(Signoff, repos) + return Signoff.objects.select_related('user').in_bulk(to_fetch).values() - results = cursor.fetchall() - to_fetch = [row[0] for row in results] + +def get_current_specifications(repos): + '''Returns a list of signoff specification objects for the given repos.''' + to_fetch = signoffs_id_query(SignoffSpecification, repos) return SignoffSpecification.objects.in_bulk(to_fetch).values() -- cgit v1.2.3-54-g00ecf From cec2b52a8d1cb321710495f02e010f5d9070975d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 14 Oct 2012 19:59:32 -0500 Subject: Convert Click & Pledge logo to PNG Signed-off-by: Dan McGee --- sitestatic/CP_EN_BK_S_001.gif | Bin 3036 -> 0 bytes sitestatic/click_and_pledge.png | Bin 0 -> 2284 bytes templates/public/donate.html | 2 +- templates/public/index.html | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 sitestatic/CP_EN_BK_S_001.gif create mode 100644 sitestatic/click_and_pledge.png diff --git a/sitestatic/CP_EN_BK_S_001.gif b/sitestatic/CP_EN_BK_S_001.gif deleted file mode 100644 index 41cf0885..00000000 Binary files a/sitestatic/CP_EN_BK_S_001.gif and /dev/null differ diff --git a/sitestatic/click_and_pledge.png b/sitestatic/click_and_pledge.png new file mode 100644 index 00000000..078bf88e Binary files /dev/null and b/sitestatic/click_and_pledge.png differ diff --git a/templates/public/donate.html b/templates/public/donate.html index b47683e7..08234006 100644 --- a/templates/public/donate.html +++ b/templates/public/donate.html @@ -31,7 +31,7 @@

    Monetary donations

    as how donations work.

    - Donate via Click&Pledge to Arch Linux + Donate via Click&Pledge to Arch Linux

    Commercial sponsors and contributions

    diff --git a/templates/public/index.html b/templates/public/index.html index cebb0681..000a527b 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -198,7 +198,7 @@

    More Resources

    -- cgit v1.2.3-54-g00ecf From e0cb92480e7d9d4b21a5b5878af6d2cf2fba34cf Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 14 Oct 2012 20:04:09 -0500 Subject: Remove print stylesheet This hasn't been updated in ages, and who is printing out pages from the website anyway? Signed-off-by: Dan McGee --- sitestatic/archweb-print.css | 11 ----------- templates/base.html | 1 - 2 files changed, 12 deletions(-) delete mode 100644 sitestatic/archweb-print.css diff --git a/sitestatic/archweb-print.css b/sitestatic/archweb-print.css deleted file mode 100644 index 2946de54..00000000 --- a/sitestatic/archweb-print.css +++ /dev/null @@ -1,11 +0,0 @@ -/* - * ARCH LINUX DJANGO (MAIN SITE) - * - * Stylesheet for printing - */ - -/* general styling */ -body { font: normal 100% sans-serif; } -#content { font-size: 0.812em; } -#archnavbarmenu, #archdev-navbar, .admin-actions { display: none; } - diff --git a/templates/base.html b/templates/base.html index e6ccc972..392bf22f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,7 +4,6 @@ {% block title %}Arch Linux{% endblock %} - -- cgit v1.2.3-54-g00ecf From 225b41d01e4e96fe5597dec400dfa7c0975dab1c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 14 Oct 2012 20:06:53 -0500 Subject: Inline global navbar stylesheet It is silly to have to load an external resource when we can simply cram all of this in the same single stylesheet used for the site. Signed-off-by: Dan McGee --- sitestatic/archweb.css | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 905a3ecb..764f4d4a 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -1,16 +1,43 @@ /* - * ARCH LINUX DJANGO (MAIN SITE) - * * Font sizing based on 16px browser defaults (use em): * 14px = 0.875em * 13px = 0.812em * 12px = 0.75em * 11px = 0.6875em - * */ -/* import the global navbar stylesheet */ -@import url('archnavbar/archnavbar.css'); +/* + * ARCH GLOBAL NAVBAR + * We're forcing all generic selectors with !important + * to help prevent other stylesheets from interfering. + */ + +/* container for the entire bar */ +#archnavbar { height: 40px !important; padding: 10px 15px !important; background: #333 !important; border-bottom: 5px #08c solid !important; } + +/* logo trickery -- GIF for IE6 and PNG for the rest */ +#archnavbarlogo { float: left !important; margin: 0 !important; padding: 0 !important; height: 40px !important; width: 190px !important; } +/* IE6 doesn't support alpha PNGs so we serve it a GIF */ +#archnavbarlogo { background: url('archlogo.gif') no-repeat !important; } +/* and use a proper PNG for all other modern browsers */ +html > body #archnavbarlogo { background: url('archlogo.png') no-repeat !important; } + +/* move the heading/paragraph text offscreen */ +#archnavbarlogo p { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; } +#archnavbarlogo h1 { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; } + +/* make the link the same size as the logo */ +#archnavbarlogo a { display: block !important; height: 40px !important; width: 190px !important; } + +/* display the list inline, float it to the right and style it */ +#archnavbar ul { display: inline !important; float: right !important; list-style: none !important; margin: 0 !important; padding: 0 !important; } +#archnavbar ul li { float: left !important; font-size: 14px !important; font-family: sans-serif !important; line-height: 45px !important; padding-right: 15px !important; padding-left: 15px !important; } + +/* style the links */ +#archnavbar ul#archnavbarlist li a { color: #999; font-weight: bold !important; text-decoration: none !important; } +#archnavbar ul li a:hover { color: white !important; text-decoration: underline !important; } + +/* END ARCH GLOBAL NAVBAR */ /* simple reset */ * { -- cgit v1.2.3-54-g00ecf From ce79a4dc0eb44be376a1e51aa38457f431954c9a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 14 Oct 2012 20:09:32 -0500 Subject: Remove logo override for IE6 We don't need this anymore. Signed-off-by: Dan McGee --- sitestatic/archnavbar/archlogo.gif | Bin 1845 -> 0 bytes sitestatic/archnavbar/archnavbar.css | 10 +--------- sitestatic/archweb.css | 8 +------- 3 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 sitestatic/archnavbar/archlogo.gif diff --git a/sitestatic/archnavbar/archlogo.gif b/sitestatic/archnavbar/archlogo.gif deleted file mode 100644 index e1852a06..00000000 Binary files a/sitestatic/archnavbar/archlogo.gif and /dev/null differ diff --git a/sitestatic/archnavbar/archnavbar.css b/sitestatic/archnavbar/archnavbar.css index d95832bc..d10c5f52 100644 --- a/sitestatic/archnavbar/archnavbar.css +++ b/sitestatic/archnavbar/archnavbar.css @@ -1,20 +1,12 @@ /* * ARCH GLOBAL NAVBAR - * * We're forcing all generic selectors with !important * to help prevent other stylesheets from interfering. - * */ /* container for the entire bar */ #archnavbar { height: 40px !important; padding: 10px 15px !important; background: #333 !important; border-bottom: 5px #08c solid !important; } - -/* logo trickery -- GIF for IE6 and PNG for the rest */ -#archnavbarlogo { float: left !important; margin: 0 !important; padding: 0 !important; height: 40px !important; width: 190px !important; } -/* IE6 doesn't support alpha PNGs so we serve it a GIF */ -#archnavbarlogo { background: url('archlogo.gif') no-repeat !important; } -/* and use a proper PNG for all other modern browsers */ -html > body #archnavbarlogo { background: url('archlogo.png') no-repeat !important; } +#archnavbarlogo { background: url('archlogo.png') no-repeat !important; } /* move the heading/paragraph text offscreen */ #archnavbarlogo p { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; } diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 764f4d4a..5c464ce3 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -14,13 +14,7 @@ /* container for the entire bar */ #archnavbar { height: 40px !important; padding: 10px 15px !important; background: #333 !important; border-bottom: 5px #08c solid !important; } - -/* logo trickery -- GIF for IE6 and PNG for the rest */ -#archnavbarlogo { float: left !important; margin: 0 !important; padding: 0 !important; height: 40px !important; width: 190px !important; } -/* IE6 doesn't support alpha PNGs so we serve it a GIF */ -#archnavbarlogo { background: url('archlogo.gif') no-repeat !important; } -/* and use a proper PNG for all other modern browsers */ -html > body #archnavbarlogo { background: url('archlogo.png') no-repeat !important; } +#archnavbarlogo { background: url('archlogo.png') no-repeat !important; } /* move the heading/paragraph text offscreen */ #archnavbarlogo p { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; } -- cgit v1.2.3-54-g00ecf From 3422efd3073df0b32833aee26a6aee7018010f4e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 18 Oct 2012 08:58:17 -0500 Subject: Modernize initial main migration Signed-off-by: Dan McGee --- main/migrations/0001_initial.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main/migrations/0001_initial.py b/main/migrations/0001_initial.py index 4c89d8a0..bc8bb492 100644 --- a/main/migrations/0001_initial.py +++ b/main/migrations/0001_initial.py @@ -1,9 +1,9 @@ - +# encoding: utf-8 from south.db import db +from south.v2 import SchemaMigration from django.db import models -from main.models import * -class Migration: +class Migration(SchemaMigration): def forwards(self, orm): -- cgit v1.2.3-54-g00ecf From abd4b6c6d8541955f327b4adc50cf2031ebe6781 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 18 Oct 2012 08:58:44 -0500 Subject: Clean up create index migration Signed-off-by: Dan McGee --- main/migrations/0004_add_pkgname_index.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/migrations/0004_add_pkgname_index.py b/main/migrations/0004_add_pkgname_index.py index 0aae4522..6e23adac 100644 --- a/main/migrations/0004_add_pkgname_index.py +++ b/main/migrations/0004_add_pkgname_index.py @@ -7,12 +7,12 @@ class Migration(SchemaMigration): def forwards(self, orm): db.alter_column('packages', 'maintainer_id', orm['main.package:maintainer']) - db.alter_column('packages', 'pkgname', orm['main.package:pkgname']) + db.create_index('packages', ['pkgname']) def backwards(self, orm): db.alter_column('packages', 'maintainer_id', orm['main.package:maintainer']) - db.alter_column('packages', 'pkgname', orm['main.package:pkgname']) + db.delete_index('packages', ['pkgname']) models = { -- cgit v1.2.3-54-g00ecf From 072b5b2a6baf2c014168e4fff1a1f2fb8f652e52 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 18 Oct 2012 09:01:46 -0500 Subject: Bump requirements Move to the 1.4.2 Django security release, and update django-countries to 1.4. Signed-off-by: Dan McGee --- requirements.txt | 4 ++-- requirements_prod.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index a80ce88a..51f15a0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -Django==1.4.1 +Django==1.4.2 Markdown==2.2.0 South==0.7.6 -django-countries==1.3 +django-countries==1.4 pgpdump==1.3 pytz>=2012f diff --git a/requirements_prod.txt b/requirements_prod.txt index f6b48205..2a13c51a 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -1,7 +1,7 @@ -Django==1.4.1 +Django==1.4.2 Markdown==2.2.0 South==0.7.6 -django-countries==1.3 +django-countries==1.4 pgpdump==1.3 psycopg2==2.4.5 pyinotify==0.9.3 -- cgit v1.2.3-54-g00ecf From ca1d4faa531768515f48b6856fda13016e12c0d3 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 21 Oct 2012 08:43:27 -0500 Subject: Update tablesorter JS Signed-off-by: Dan McGee --- main/templatetags/cdn.py | 2 +- sitestatic/jquery.tablesorter-2.3.11.js | 1181 ------------------------ sitestatic/jquery.tablesorter-2.3.11.min.js | 6 - sitestatic/jquery.tablesorter-2.4.5.js | 1333 +++++++++++++++++++++++++++ sitestatic/jquery.tablesorter-2.4.5.min.js | 6 + 5 files changed, 1340 insertions(+), 1188 deletions(-) delete mode 100644 sitestatic/jquery.tablesorter-2.3.11.js delete mode 100644 sitestatic/jquery.tablesorter-2.3.11.min.js create mode 100644 sitestatic/jquery.tablesorter-2.4.5.js create mode 100644 sitestatic/jquery.tablesorter-2.4.5.min.js diff --git a/main/templatetags/cdn.py b/main/templatetags/cdn.py index b1c65818..f3ef9a1b 100644 --- a/main/templatetags/cdn.py +++ b/main/templatetags/cdn.py @@ -20,7 +20,7 @@ def jquery(): @register.simple_tag def jquery_tablesorter(): - version = '2.3.11' + version = '2.4.5' filename = 'jquery.tablesorter-%s.min.js' % version link = staticfiles_storage.url(filename) return '' % link diff --git a/sitestatic/jquery.tablesorter-2.3.11.js b/sitestatic/jquery.tablesorter-2.3.11.js deleted file mode 100644 index e52e66dc..00000000 --- a/sitestatic/jquery.tablesorter-2.3.11.js +++ /dev/null @@ -1,1181 +0,0 @@ -/*! -* TableSorter 2.3.11 - Client-side table sorting with ease! -* @requires jQuery v1.2.6+ -* -* Copyright (c) 2007 Christian Bach -* Examples and docs at: http://tablesorter.com -* Dual licensed under the MIT and GPL licenses: -* http://www.opensource.org/licenses/mit-license.php -* http://www.gnu.org/licenses/gpl.html -* -* @type jQuery -* @name tablesorter -* @cat Plugins/Tablesorter -* @author Christian Bach/christian.bach@polyester.se -* @contributor Rob Garrison/https://github.com/Mottie/tablesorter -*/ -!(function($) { - $.extend({ - tablesorter: new function() { - - this.version = "2.3.11"; - - var parsers = [], widgets = []; - this.defaults = { - - // appearance - widthFixed : false, // adds colgroup to fix widths of columns - - // functionality - cancelSelection : true, // prevent text selection in the header - dateFormat : "mmddyyyy", // other options: "ddmmyyy" or "yyyymmdd" - sortMultiSortKey : "shiftKey", // key used to select additional columns - usNumberFormat : true, // false for German "1.234.567,89" or French "1 234 567,89" - delayInit : false, // if false, the parsed table contents will not update until the first sort. - - // sort options - headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc. - ignoreCase : true, // ignore case while sorting - sortForce : null, // column(s) first sorted; always applied - sortList : [], // Initial sort order; applied initially; updated when manually sorted - sortAppend : null, // column(s) sorted last; always applied - - sortInitialOrder : "asc", // sort direction on first click - sortLocaleCompare: false, // replace equivalent character (accented characters) - sortReset : false, // third click on the header will reset column to default - unsorted - sortRestart : false, // restart sort to "sortInitialOrder" when clicking on previously unsorted columns - - emptyTo : "bottom", // sort empty cell to bottom, top, none, zero - stringTo : "max", // sort strings in numerical column as max, min, top, bottom, zero - textExtraction : "simple", // text extraction method/function - function(node, table, cellIndex){} - textSorter : null, // use custom text sorter - function(a,b){ return a.sort(b); } // basic sort - - // widget options - widgets: [], // method to add widgets, e.g. widgets: ['zebra'] - widgetOptions : { - zebra : [ "even", "odd" ] // zebra widget alternating row class names - }, - initWidgets : true, // apply widgets on tablesorter initialization - - // callbacks - initialized : null, // function(table){}, - onRenderHeader : null, // function(index){}, - - // css class names - tableClass : 'tablesorter', - cssAsc : "tablesorter-headerSortUp", - cssChildRow : "expand-child", - cssDesc : "tablesorter-headerSortDown", - cssHeader : "tablesorter-header", - cssInfoBlock : "tablesorter-infoOnly", // don't sort tbody with this class name - - // selectors - selectorHeaders : '> thead th', - selectorRemove : "tr.remove-me", - - // advanced - debug : false, - - // Internal variables - headerList: [], - empties: {}, - strings: {}, - parsers: [] - - // deprecated; but retained for backwards compatibility - // widgetZebra: { css: ["even", "odd"] } - - }; - - /* debuging utils */ - function log(s) { - if (typeof console !== "undefined" && typeof console.log !== "undefined") { - console.log(s); - } else { - alert(s); - } - } - - function benchmark(s, d) { - log(s + " (" + (new Date().getTime() - d.getTime()) + "ms)"); - } - - this.benchmark = benchmark; - this.hasInitialized = false; - - function getElementText(table, node, cellIndex) { - if (!node) { return ""; } - var c = table.config, - t = c.textExtraction, text = ""; - if (t === "simple") { - if (c.supportsTextContent) { - text = node.textContent; // newer browsers support this - } else { - text = $(node).text(); - } - } else { - if (typeof(t) === "function") { - text = t(node, table, cellIndex); - } else if (typeof(t) === "object" && t.hasOwnProperty(cellIndex)) { - text = t[cellIndex](node, table, cellIndex); - } else { - text = c.supportsTextContent ? node.textContent : $(node).text(); - } - } - return $.trim(text); - } - - /* parsers utils */ - function getParserById(name) { - var i, l = parsers.length; - for (i = 0; i < l; i++) { - if (parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) { - return parsers[i]; - } - } - return false; - } - - function detectParserForColumn(table, rows, rowIndex, cellIndex) { - var i, l = parsers.length, - node = false, - nodeValue = '', - keepLooking = true; - while (nodeValue === '' && keepLooking) { - rowIndex++; - if (rows[rowIndex]) { - node = rows[rowIndex].cells[cellIndex]; - nodeValue = getElementText(table, node, cellIndex); - if (table.config.debug) { - log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': ' + nodeValue); - } - } else { - keepLooking = false; - } - } - for (i = 1; i < l; i++) { - if (parsers[i].is(nodeValue, table, node)) { - return parsers[i]; - } - } - // 0 is always the generic parser (text) - return parsers[0]; - } - - function buildParserCache(table, $headers) { - var c = table.config, - tb = $(table.tBodies).filter(':not(.' + c.cssInfoBlock + ')'), - ts = $.tablesorter, rows, list, l, i, h, m, ch, cl, p, parsersDebug = ""; - if ( tb.length === 0) { return; } // In the case of empty tables - rows = tb[0].rows; - if (rows[0]) { - list = []; - l = rows[0].cells.length; - for (i = 0; i < l; i++) { - // tons of thanks to AnthonyM1229 for working out the following selector (issue #74) to make this work in IE8! - h = $headers.filter(':not([colspan])[data-column="' + i + '"]:last,[colspan="1"][data-column="' + i + '"]:last'); - ch = c.headers[i]; - // get column parser - p = getParserById( ts.getData(h, ch, 'sorter') ); - // empty cells behaviour - keeping emptyToBottom for backwards compatibility. - c.empties[i] = ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' ); - // text strings behaviour in numerical sorts - c.strings[i] = ts.getData(h, ch, 'string') || c.stringTo || 'max'; - if (!p) { - p = detectParserForColumn(table, rows, -1, i); - } - if (c.debug) { - parsersDebug += "column:" + i + "; parser:" + p.id + "; string:" + c.strings[i] + '; empty: ' + c.empties[i] + "\n"; - } - list.push(p); - } - } - if (c.debug) { - log(parsersDebug); - } - return list; - } - - /* utils */ - function buildCache(table) { - var b = table.tBodies, - tc = table.config, - totalRows, - totalCells, - parsers = tc.parsers, - t, i, j, k, c, cols, cacheTime; - tc.cache = {}; - if (tc.debug) { - cacheTime = new Date(); - } - for (k = 0; k < b.length; k++) { - tc.cache[k] = { row: [], normalized: [] }; - // ignore tbodies with class name from css.cssInfoBlock - if (!$(b[k]).hasClass(tc.cssInfoBlock)) { - $(b[k]).addClass('tablesorter-hidden'); - totalRows = (b[k] && b[k].rows.length) || 0; - totalCells = (b[k].rows[0] && b[k].rows[0].cells.length) || 0; - for (i = 0; i < totalRows; ++i) { - /** Add the table data to main data array */ - c = $(b[k].rows[i]); - cols = []; - // if this is a child row, add it to the last row's children and continue to the next row - if (c.hasClass(tc.cssChildRow)) { - tc.cache[k].row[tc.cache[k].row.length - 1] = tc.cache[k].row[tc.cache[k].row.length - 1].add(c); - // go to the next for loop - continue; - } - tc.cache[k].row.push(c); - for (j = 0; j < totalCells; ++j) { - t = getElementText(table, c[0].cells[j], j); - // allow parsing if the string is empty, previously parsing would change it to zero, - // in case the parser needs to extract data from the table cell attributes - cols.push( parsers[j].format(t, table, c[0].cells[j], j) ); - } - cols.push(tc.cache[k].normalized.length); // add position for rowCache - tc.cache[k].normalized.push(cols); - } - $(b[k]).removeClass('tablesorter-hidden'); - } - } - if (tc.debug) { - benchmark("Building cache for " + totalRows + " rows", cacheTime); - } - } - - function getWidgetById(name) { - var i, w, l = widgets.length; - for (i = 0; i < l; i++) { - w = widgets[i]; - if (w && w.hasOwnProperty('id') && w.id.toLowerCase() === name.toLowerCase()) { - return w; - } - } - } - - function applyWidget(table, init) { - var tc = table.config, c = tc.widgets, - time, i, w, l = c.length; - if (tc.debug) { - time = new Date(); - } - for (i = 0; i < l; i++) { - w = getWidgetById(c[i]); - if ( w ) { - if (init === true && w.hasOwnProperty('init')) { - w.init(table, widgets, w); - } else if (!init && w.hasOwnProperty('format')) { - w.format(table); - } - } - } - if (tc.debug) { - benchmark("Completed " + (init === true ? "initializing" : "applying") + " widgets", time); - } - } - - // init flag (true) used by pager plugin to prevent widget application - function appendToTable(table, init) { - var c = table.config, - b = table.tBodies, - rows = [], - r, n, totalRows, checkCell, c2 = c.cache, - f, i, j, k, l, pos, appendTime; - if (c.debug) { - appendTime = new Date(); - } - for (k = 0; k < b.length; k++) { - if (!$(b[k]).hasClass(c.cssInfoBlock)){ - $(b[k]).addClass('tablesorter-hidden'); - f = document.createDocumentFragment(); - r = c2[k].row; - n = c2[k].normalized; - totalRows = n.length; - checkCell = totalRows ? (n[0].length - 1) : 0; - for (i = 0; i < totalRows; i++) { - pos = n[i][checkCell]; - rows.push(r[pos]); - // removeRows used by the pager plugin - if (!c.appender || !c.removeRows) { - l = r[pos].length; - for (j = 0; j < l; j++) { - f.appendChild(r[pos][j]); - } - } - } - table.tBodies[k].appendChild(f); - $(b[k]).removeClass('tablesorter-hidden'); - } - } - if (c.appender) { - c.appender(table, rows); - } - if (c.debug) { - benchmark("Rebuilt table", appendTime); - } - // apply table widgets - if (!init) { applyWidget(table); } - // trigger sortend - $(table).trigger("sortEnd", table); - } - - // computeTableHeaderCellIndexes from: - // http://www.javascripttoolbox.com/lib/table/examples.php - // http://www.javascripttoolbox.com/temp/table_cellindex.html - function computeThIndexes(t) { - var matrix = [], - lookup = {}, - trs = $(t).find('thead:eq(0) tr'), - i, j, k, l, c, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol, matrixrow; - for (i = 0; i < trs.length; i++) { - cells = trs[i].cells; - for (j = 0; j < cells.length; j++) { - c = cells[j]; - rowIndex = c.parentNode.rowIndex; - cellId = rowIndex + "-" + c.cellIndex; - rowSpan = c.rowSpan || 1; - colSpan = c.colSpan || 1; - if (typeof(matrix[rowIndex]) === "undefined") { - matrix[rowIndex] = []; - } - // Find first available column in the first row - for (k = 0; k < matrix[rowIndex].length + 1; k++) { - if (typeof(matrix[rowIndex][k]) === "undefined") { - firstAvailCol = k; - break; - } - } - lookup[cellId] = firstAvailCol; - // add data-column - $(c).attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex - for (k = rowIndex; k < rowIndex + rowSpan; k++) { - if (typeof(matrix[k]) === "undefined") { - matrix[k] = []; - } - matrixrow = matrix[k]; - for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) { - matrixrow[l] = "x"; - } - } - } - } - return lookup; - } - - function formatSortingOrder(v) { - // look for "d" in "desc" order; return true - return (/^d/i.test(v) || v === 1); - } - - - function buildHeaders(table) { - var header_index = computeThIndexes(table), ch, $t, - $th, lock, time, $tableHeaders, c = table.config, ts = $.tablesorter; - c.headerList = []; - if (c.debug) { - time = new Date(); - } - $tableHeaders = $(table).find(c.selectorHeaders) - .each(function(index) { - $t = $(this); - ch = c.headers[index]; - this.innerHTML = '
    ' + this.innerHTML + '
    '; // faster than wrapInner - if (c.onRenderHeader) { c.onRenderHeader.apply($t, [index]); } - this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; - this.order = formatSortingOrder( ts.getData($t, ch, 'sortInitialOrder') || c.sortInitialOrder ) ? [1,0,2] : [0,1,2]; - this.count = -1; // set to -1 because clicking on the header automatically adds one - if (ts.getData($t, ch, 'sorter') === 'false') { this.sortDisabled = true; } - this.lockedOrder = false; - lock = ts.getData($t, ch, 'lockedOrder') || false; - if (typeof(lock) !== 'undefined' && lock !== false) { - this.order = this.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0]; - } - if (!this.sortDisabled) { - $th = $t.addClass(c.cssHeader); - } - // add cell to headerList - c.headerList[index] = this; - // add to parent in case there are multiple rows - $t.parent().addClass(c.cssHeader); - }); - if (table.config.debug) { - benchmark("Built headers:", time); - log($tableHeaders); - } - return $tableHeaders; - } - - function isValueInArray(v, a) { - var i, l = a.length; - for (i = 0; i < l; i++) { - if (a[i][0] === v) { - return true; - } - } - return false; - } - - function setHeadersCss(table, $headers, list) { - var f, h = [], i, j, l, css = [table.config.cssDesc, table.config.cssAsc]; - // remove all header information - $headers - .removeClass(css.join(' ')) - .each(function() { - if (!this.sortDisabled) { - h[this.column] = $(this); - } - }); - l = list.length; - for (i = 0; i < l; i++) { - if (list[i][1] === 2) { continue; } // direction = 2 means reset! - if (h[list[i][0]]) { - // add class if cell exists - fix for issue #78 - h[list[i][0]].addClass(css[list[i][1]]); - } - // multicolumn sorting updating - f = $headers.filter('[data-column="' + list[i][0] + '"]'); - if (l > 1 && f.length) { - for (j = 0; j < f.length; j++) { - if (!f[j].sortDisabled) { - $(f[j]).addClass(css[list[i][1]]); - } - } - } - } - } - - function fixColumnWidth(table) { - if (table.config.widthFixed) { - var colgroup = $('
    '); - $("tr:first td", table.tBodies[0]).each(function() { - colgroup.append($('').css('width', $(this).width())); - }); - $(table).prepend(colgroup); - } - } - - function updateHeaderSortCount(table, sortList) { - var i, s, o, c = table.config, - l = sortList.length; - for (i = 0; i < l; i++) { - s = sortList[i]; - o = c.headerList[s[0]]; - if (o) { // prevents error if sorton array is wrong - o.count = s[1] % (c.sortReset ? 3 : 2); - } - } - } - - function getCachedSortType(parsers, i) { - return (parsers && parsers[i]) ? parsers[i].type || '' : ''; - } - - /* sorting methods - reverted sorting method back to version 2.0.3 */ - function multisort(table, sortList) { - var dynamicExp, sortWrapper, col, mx = 0, dir = 0, tc = table.config, - l = sortList.length, bl = table.tBodies.length, - sortTime, i, j, k, c, cache, lc, s, e, order, orgOrderCol; - if (tc.debug) { sortTime = new Date(); } - for (k = 0; k < bl; k++) { - dynamicExp = "sortWrapper = function(a,b) {"; - cache = tc.cache[k]; - lc = cache.normalized.length; - for (i = 0; i < l; i++) { - c = sortList[i][0]; - order = sortList[i][1]; - // fallback to natural sort since it is more robust - s = /n/i.test(getCachedSortType(tc.parsers, c)) ? "Numeric" : "Text"; - s += order === 0 ? "" : "Desc"; - e = "e" + i; - // get max column value (ignore sign) - if (/Numeric/.test(s) && tc.strings[c]) { - for (j = 0; j < lc; j++) { - col = Math.abs(parseFloat(cache.normalized[j][c])); - mx = Math.max( mx, isNaN(col) ? 0 : col ); - } - // sort strings in numerical columns - if (typeof(tc.string[tc.strings[c]]) === 'boolean') { - dir = (order === 0 ? 1 : -1) * (tc.string[tc.strings[c]] ? -1 : 1); - } else { - dir = (tc.strings[c]) ? tc.string[tc.strings[c]] || 0 : 0; - } - } - dynamicExp += "var " + e + " = $.tablesorter.sort" + s + "(table,a[" + c + "],b[" + c + "]," + c + "," + mx + "," + dir + "); "; - dynamicExp += "if (" + e + ") { return " + e + "; } "; - dynamicExp += "else { "; - } - // if value is the same keep orignal order - orgOrderCol = (cache.normalized && cache.normalized[0]) ? cache.normalized[0].length - 1 : 0; - dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];"; - for (i=0; i < l; i++) { - dynamicExp += "}; "; - } - dynamicExp += "return 0; "; - dynamicExp += "}; "; - cache.normalized.sort(eval(dynamicExp)); // sort using eval expression - } - if (tc.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order+ " time", sortTime); } - } - - function checkResort($table, flag, callback) { - var t = $table[0]; - if (flag !== false) { - $table.trigger("sorton", [t.config.sortList, function(){ - $table.trigger('updateComplete'); - if (typeof callback === "function") { - callback(t); - } - }]); - } else { - $table.trigger('updateComplete'); - if (typeof callback === "function") { - callback(t); - } - } - } - - /* public methods */ - this.construct = function(settings) { - return this.each(function() { - // if no thead or tbody quit. - if (!this.tHead || this.tBodies.length === 0) { return; } - // declare - var $headers, $cell, $this, - c, i, j, k, a, s, o, downTime, - m = $.metadata; - // new blank config object - this.config = {}; - // merge and extend. - c = $.extend(true, this.config, $.tablesorter.defaults, settings); - - if (c.debug) { $.data( this, 'startoveralltimer', new Date()); } - // store common expression for speed - $this = $(this).addClass(c.tableClass); - // save the settings where they read - $.data(this, "tablesorter", c); - c.supportsTextContent = $('x')[0].textContent === 'x'; - // digit sort text location; keeping max+/- for backwards compatibility - c.string = { 'max': 1, 'min': -1, 'max+': 1, 'max-': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false }; - // build headers - $headers = buildHeaders(this); - // try to auto detect column type, and store in tables config - c.parsers = buildParserCache(this, $headers); - // build the cache for the tbody cells - // delayInit will delay building the cache until the user starts a sort - if (!c.delayInit) { buildCache(this); } - // fixate columns if the users supplies the fixedWidth option - fixColumnWidth(this); - // apply event handling to headers - // this is to big, perhaps break it out? - $headers.bind('mousedown.tablesorter mouseup.tablesorter', function(e, external) { - if (e.type === 'mousedown') { - downTime = new Date().getTime(); - return !c.cancelSelection; - } - // prevent resizable widget from initializing a sort (long clicks are ignored) - if (external !== true && (new Date().getTime() - downTime > 500)) { return false; } - if (c.delayInit && !c.cache) { buildCache($this[0]); } - if (!this.sortDisabled) { - // Only call sortStart if sorting is enabled. - $this.trigger("sortStart", $this[0]); - // store exp, for speed - $cell = $(this); - k = !e[c.sortMultiSortKey]; - // get current column sort order - this.count = (this.count + 1) % (c.sortReset ? 3 : 2); - // reset all sorts on non-current column - issue #30 - if (c.sortRestart) { - i = this; - $headers.each(function() { - // only reset counts on columns that weren't just clicked on and if not included in a multisort - if (this !== i && (k || !$(this).is('.' + c.cssDesc + ',.' + c.cssAsc))) { - this.count = -1; - } - }); - } - // get current column index - i = this.column; - // user only wants to sort on one column - if (k) { - // flush the sort list - c.sortList = []; - if (c.sortForce !== null) { - a = c.sortForce; - for (j = 0; j < a.length; j++) { - if (a[j][0] !== i) { - c.sortList.push(a[j]); - } - } - } - // add column to sort list - o = this.order[this.count]; - if (o < 2) { - c.sortList.push([i, o]); - // add other columns if header spans across multiple - if (this.colSpan > 1) { - for (j = 1; j < this.colSpan; j++) { - c.sortList.push([i + j, o]); - } - } - } - // multi column sorting - } else { - // the user has clicked on an already sorted column. - if (isValueInArray(i, c.sortList)) { - // reverse the sorting direction for all tables. - for (j = 0; j < c.sortList.length; j++) { - s = c.sortList[j]; - o = c.headerList[s[0]]; - if (s[0] === i) { - s[1] = o.order[o.count]; - if (s[1] === 2) { - c.sortList.splice(j,1); - o.count = -1; - } - } - } - } else { - // add column to sort list array - o = this.order[this.count]; - if (o < 2) { - c.sortList.push([i, o]); - // add other columns if header spans across multiple - if (this.colSpan > 1) { - for (j = 1; j < this.colSpan; j++) { - c.sortList.push([i + j, o]); - } - } - } - } - } - if (c.sortAppend !== null) { - a = c.sortAppend; - for (j = 0; j < a.length; j++) { - if (a[j][0] !== i) { - c.sortList.push(a[j]); - } - } - } - // sortBegin event triggered immediately before the sort - $this.trigger("sortBegin", $this[0]); - // set css for headers - setHeadersCss($this[0], $headers, c.sortList); - multisort($this[0], c.sortList); - appendToTable($this[0]); - } - }); - if (c.cancelSelection) { - // cancel selection - $headers.each(function() { - this.onselectstart = function() { - return false; - }; - }); - } - // apply easy methods that trigger binded events - $this - .bind("update", function(e, resort, callback) { - // remove rows/elements before update - $(c.selectorRemove, this).remove(); - // rebuild parsers. - c.parsers = buildParserCache(this, $headers); - // rebuild the cache map - buildCache(this); - checkResort($this, resort, callback); - }) - .bind("updateCell", function(e, cell, resort, callback) { - // get position from the dom. - var t = this, $tb = $(this).find('tbody'), row, pos, - // update cache - format: function(s, table, cell, cellIndex) - tbdy = $tb.index( $(cell).closest('tbody') ); - row = $tb.eq(tbdy).find('tr').index( $(cell).closest('tr') ); - pos = [ row, cell.cellIndex]; - t.config.cache[tbdy].normalized[pos[0]][pos[1]] = c.parsers[pos[1]].format( getElementText(t, cell, pos[1]), t, cell, pos[1] ); - checkResort($this, resort, callback); - }) - .bind("addRows", function(e, $row, resort, callback) { - var i, rows = $row.filter('tr').length, - dat = [], l = $row[0].cells.length, t = this, - tbdy = $(this).find('tbody').index( $row.closest('tbody') ); - // add each row - for (i = 0; i < rows; i++) { - // add each cell - for (j = 0; j < l; j++) { - dat[j] = c.parsers[j].format( getElementText(t, $row[i].cells[j], j), t, $row[i].cells[j], j ); - } - // add the row index to the end - dat.push(c.cache[tbdy].row.length); - // update cache - c.cache[tbdy].row.push([$row[i]]); - c.cache[tbdy].normalized.push(dat); - dat = []; - } - // resort using current settings - checkResort($this, resort, callback); - }) - .bind("sorton", function(e, list, callback, init) { - $(this).trigger("sortStart", this); - var l = c.headerList.length; - c.sortList = []; - $.each(list, function(i,v){ - // make sure column exists - if (v[0] < l) { c.sortList.push(list[i]); } - }); - // update header count index - updateHeaderSortCount(this, c.sortList); - // set css for headers - setHeadersCss(this, $headers, c.sortList); - // sort the table and append it to the dom - multisort(this, c.sortList); - appendToTable(this, init); - if (typeof callback === "function") { - callback(this); - } - }) - .bind("appendCache", function(e, init) { - appendToTable(this, init); - }) - .bind("applyWidgetId", function(e, id) { - getWidgetById(id).format(this); - }) - .bind("applyWidgets", function(e, init) { - // apply widgets - applyWidget(this, init); - }) - .bind("destroy", function(e,c){ - $.tablesorter.destroy(this, c); - }); - - // get sort list from jQuery data or metadata - if ($this.data() && typeof $this.data().sortlist !== 'undefined') { - c.sortList = $this.data().sortlist; - } else if (m && ($this.metadata() && $this.metadata().sortlist)) { - c.sortList = $this.metadata().sortlist; - } - // apply widget init code - applyWidget(this, true); - // if user has supplied a sort list to constructor. - if (c.sortList.length > 0) { - $this.trigger("sorton", [c.sortList, {}, !c.initWidgets]); - } else if (c.initWidgets) { - // apply widget format - applyWidget(this); - } - - // initialized - this.hasInitialized = true; - if (c.debug) { - $.tablesorter.benchmark("Overall initialization time", $.data( this, 'startoveralltimer')); - } - $this.trigger('tablesorter-initialized', this); - if (typeof c.initialized === 'function') { c.initialized(this); } - }); - }; - - // Natural sort - https://github.com/overset/javascript-natural-sort - this.sortText = function(table, a, b, col) { - if (a === b) { return 0; } - var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ], - r = $.tablesorter.regex, xN, xD, yN, yD, xF, yF, i, mx; - if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } - if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } - if (typeof c.textSorter === 'function') { return c.textSorter(a, b, table, col); } - // chunk/tokenize - xN = a.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); - yN = b.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); - // numeric, hex or date detection - xD = parseInt(a.match(r[2]),16) || (xN.length !== 1 && a.match(r[1]) && Date.parse(a)); - yD = parseInt(b.match(r[2]),16) || (xD && b.match(r[1]) && Date.parse(b)) || null; - // first try and sort Hex codes or Dates - if (yD) { - if ( xD < yD ) { return -1; } - if ( xD > yD ) { return 1; } - } - mx = Math.max(xN.length, yN.length); - // natural sorting through split numeric strings and default strings - for (i = 0; i < mx; i++) { - // find floats not starting with '0', string or 0 if not defined (Clint Priest) - xF = (!(xN[i] || '').match(r[3]) && parseFloat(xN[i])) || xN[i] || 0; - yF = (!(yN[i] || '').match(r[3]) && parseFloat(yN[i])) || yN[i] || 0; - // handle numeric vs string comparison - number < string - (Kyle Adams) - if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; } - // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' - if (typeof xF !== typeof yF) { - xF += ''; - yF += ''; - } - if (xF < yF) { return -1; } - if (xF > yF) { return 1; } - } - return 0; - }; - - this.sortTextDesc = function(table, a, b, col) { - if (a === b) { return 0; } - var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; - if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } - if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } - if (typeof c.textSorter === 'function') { return c.textSorter(b, a, table, col); } - return this.sortText(table, b, a); - }; - - // return text string value by adding up ascii value - // so the text is somewhat sorted when using a digital sort - // this is NOT an alphanumeric sort - this.getTextValue = function(a, mx, d) { - if (mx) { - // make sure the text value is greater than the max numerical value (mx) - var i, l = a.length, n = mx + d; - for (i = 0; i < l; i++) { - n += a.charCodeAt(i); - } - return d * n; - } - return 0; - }; - - this.sortNumeric = function(table, a, b, col, mx, d) { - if (a === b) { return 0; } - var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; - if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } - if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } - if (isNaN(a)) { a = this.getTextValue(a, mx, d); } - if (isNaN(b)) { b = this.getTextValue(b, mx, d); } - return a - b; - }; - - this.sortNumericDesc = function(table, a, b, col, mx, d) { - if (a === b) { return 0; } - var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; - if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } - if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } - if (isNaN(a)) { a = this.getTextValue(a, mx, d); } - if (isNaN(b)) { b = this.getTextValue(b, mx, d); } - return b - a; - }; - - this.destroy = function(table, removeClasses){ - var $t = $(table), c = table.config; - // remove widget added rows - $t.find('thead:first tr:not(.' + c.cssHeader + ')').remove(); - // remove resizer widget stuff - $t.find('thead:first .tablesorter-resizer').remove(); - // disable tablesorter - $t - .unbind('update updateCell addRows sorton appendCache applyWidgetId applyWidgets destroy mouseup mouseleave') - .find(c.selectorHeaders) - .unbind('click mousedown mousemove mouseup') - .removeClass(c.cssHeader + ' ' + c.cssAsc + ' ' + c.cssDesc); - if (removeClasses !== false) { - $t.removeClass(c.tableClass); - } - }; - - this.addParser = function(parser) { - var i, l = parsers.length, a = true; - for (i = 0; i < l; i++) { - if (parsers[i].id.toLowerCase() === parser.id.toLowerCase()) { - a = false; - } - } - if (a) { - parsers.push(parser); - } - }; - this.addWidget = function(widget) { - widgets.push(widget); - }; - - this.formatFloat = function(s, table) { - if (typeof(s) !== 'string' || s === '') { return s; } - if (table.config.usNumberFormat !== false) { - // US Format - 1,234,567.89 -> 1234567.89 - s = s.replace(/,/g,''); - } else { - // German Format = 1.234.567,89 -> 1234567.89 - // French Format = 1 234 567,89 -> 1234567.89 - s = s.replace(/[\s|\.]/g,'').replace(/,/g,'.'); - } - if(/^\s*\([.\d]+\)/.test(s)) { - s = s.replace(/^\s*\(/,'-').replace(/\)/,''); - } - var i = parseFloat(s); - // return the text instead of zero - return isNaN(i) ? $.trim(s) : i; - }; - this.isDigit = function(s) { - // replace all unwanted chars and match. - return (/^[\-+(]?\d+[)]?$/).test(s.replace(/[,.'\s]/g, '')); - }; - - // regex used in natural sort - this.regex = [ - /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi, // chunk/tokenize numbers & letters - /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/, //date - /^0x[0-9a-f]+$/i, // hex - /^0/ // leading zeros - ]; - // used when replacing accented characters during sorting - this.characterEquivalents = { - "a" : "\u00e1\u00e0\u00e2\u00e3\u00e4", // áàâãä - "A" : "\u00c1\u00c0\u00c2\u00c3\u00c4", // ÁÀÂÃÄ - "c" : "\u00e7", // ç - "C" : "\u00c7", // Ç - "e" : "\u00e9\u00e8\u00ea\u00eb", // éèêë - "E" : "\u00c9\u00c8\u00ca\u00cb", // ÉÈÊË - "i" : "\u00ed\u00ec\u0130\u00ee\u00ef", // íìİîï - "I" : "\u00cd\u00cc\u0130\u00ce\u00cf", // ÍÌİÎÏ - "o" : "\u00f3\u00f2\u00f4\u00f5\u00f6", // óòôõö - "O" : "\u00d3\u00d2\u00d4\u00d5\u00d6", // ÓÒÔÕÖ - "S" : "\u00df", // ß - "u" : "\u00fa\u00f9\u00fb\u00fc", // úùûü - "U" : "\u00da\u00d9\u00db\u00dc" // ÚÙÛÜ - }; - this.replaceAccents = function(s) { - var a, acc = '[', eq = this.characterEquivalents; - if (!this.characterRegex) { - this.characterRegexArray = {}; - for (a in eq) { - if (typeof a === 'string') { - acc += eq[a]; - this.characterRegexArray[a] = new RegExp('[' + eq[a] + ']', 'g'); - } - } - this.characterRegex = new RegExp(acc + ']'); - } - if (this.characterRegex.test(s)) { - for (a in eq) { - if (typeof a === 'string') { - s = s.replace( this.characterRegexArray[a], a ); - } - } - } - return s; - }; - - // get sorter, string, empty, etc options for each column from - // jQuery data, metadata, header option or header class name ("sorter-false") - // priority = jQuery data > meta > headers option > header class name - this.getData = function(h, ch, key) { - var val = '', $h = $(h), m, cl; - if (!$h.length) { return ''; } - m = $.metadata ? $h.metadata() : false; - cl = ' ' + ($h.attr('class') || ''); - if ($h.data() && ( typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined') ){ - // "data-lockedOrder" is assigned to "lockedorder"; but "data-locked-order" is assigned to "lockedOrder" - // "data-sort-initial-order" is assigned to "sortInitialOrder" - val += $h.data(key) || $h.data(key.toLowerCase()); - } else if (m && typeof m[key] !== 'undefined') { - val += m[key]; - } else if (ch && typeof ch[key] !== 'undefined') { - val += ch[key]; - } else if (cl && cl.match(' ' + key + '-')) { - // include sorter class name "sorter-text", etc - val = cl.match( new RegExp(' ' + key + '-(\\w+)') )[1] || ''; - } - return $.trim(val); - }; - - this.clearTableBody = function(table) { - $(table.tBodies).filter(':not(.' + table.config.cssInfoBlock + ')').empty(); - }; - - } - })(); - - // make shortcut - var ts = $.tablesorter; - - // extend plugin scope - $.fn.extend({ - tablesorter: ts.construct - }); - - // add default parsers - ts.addParser({ - id: "text", - is: function(s, table, node) { - return true; - }, - format: function(s, table, cell, cellIndex) { - var c = table.config; - s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s ); - return c.sortLocaleCompare ? ts.replaceAccents(s) : s; - }, - type: "text" - }); - - ts.addParser({ - id: "currency", - is: function(s) { - return (/^\(?[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+/).test(s); // £$€¤¥¢ - }, - format: function(s, table) { - return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "ipAddress", - is: function(s) { - return (/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/).test(s); - }, - format: function(s, table) { - var i, a = s.split("."), - r = "", - l = a.length; - for (i = 0; i < l; i++) { - r += ("00" + a[i]).slice(-3); - } - return ts.formatFloat(r, table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "url", - is: function(s) { - return (/^(https?|ftp|file):\/\//).test(s); - }, - format: function(s) { - return $.trim(s.replace(/(https?|ftp|file):\/\//, '')); - }, - type: "text" - }); - - ts.addParser({ - id: "isoDate", - is: function(s) { - return (/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/).test(s); - }, - format: function(s, table) { - return ts.formatFloat((s !== "") ? (new Date(s.replace(/-/g, "/")).getTime() || "") : "", table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "percent", - is: function(s) { - return (/\d%\)?$/).test(s); - }, - format: function(s, table) { - return ts.formatFloat(s.replace(/%/g, ""), table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "usLongDate", - is: function(s) { - return s.match(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/); - }, - format: function(s, table) { - return ts.formatFloat( (new Date(s).getTime() || ''), table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "shortDate", // "mmddyyyy", "ddmmyyyy" or "yyyymmdd" - is: function(s) { - // testing for ####-##-#### - so it's not perfect - return (/^(\d{2}|\d{4})[\/\-\,\.\s+]\d{2}[\/\-\.\,\s+](\d{2}|\d{4})$/).test(s); - }, - format: function(s, table, cell, cellIndex) { - var c = table.config, ci = c.headerList[cellIndex], - format = ci.shortDateFormat; - if (typeof format === 'undefined') { - // cache header formatting so it doesn't getData for every cell in the column - format = ci.shortDateFormat = ts.getData( ci, c.headers[cellIndex], 'dateFormat') || c.dateFormat; - } - s = s.replace(/\s+/g," ").replace(/[\-|\.|\,]/g, "/"); - if (format === "mmddyyyy") { - s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$1/$2"); - } else if (format === "ddmmyyyy") { - s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$2/$1"); - } else if (format === "yyyymmdd") { - s = s.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/, "$1/$2/$3"); - } - return ts.formatFloat( (new Date(s).getTime() || ''), table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "time", - is: function(s) { - return (/^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/).test(s); - }, - format: function(s, table) { - return ts.formatFloat( (new Date("2000/01/01 " + s).getTime() || ''), table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "digit", - is: function(s) { - return ts.isDigit(s); - }, - format: function(s, table) { - return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "metadata", - is: function(s) { - return false; - }, - format: function(s, table, cell) { - var c = table.config, - p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; - return $(cell).metadata()[p]; - }, - type: "numeric" - }); - - // add default widgets - ts.addWidget({ - id: "zebra", - format: function(table) { - var $tb, $tv, $tr, row, even, time, k, l, - c = table.config, - child = new RegExp(c.cssChildRow, 'i'), - b = $(table).children('tbody:not(.' + c.cssInfoBlock + ')'), - css = [ "even", "odd" ]; - // maintain backwards compatibility - css = c.widgetZebra && c.hasOwnProperty('css') ? c.widgetZebra.css : - (c.widgetOptions && c.widgetOptions.hasOwnProperty('zebra')) ? c.widgetOptions.zebra : css; - if (c.debug) { - time = new Date(); - } - for (k = 0; k < b.length; k++ ) { - // loop through the visible rows - $tb = $(b[k]); - l = $tb.children('tr').length; - if (l > 1) { - row = 0; - $tv = $tb.find('tr:visible'); - $tb.addClass('tablesorter-hidden'); - // revered back to using jQuery each - strangely it's the fastest method - $tv.each(function(){ - $tr = $(this); - // style children rows the same way the parent row was styled - if (!child.test(this.className)) { row++; } - even = (row % 2 === 0); - $tr.removeClass(css[even ? 1 : 0]).addClass(css[even ? 0 : 1]); - }); - $tb.removeClass('tablesorter-hidden'); - } - } - if (c.debug) { - ts.benchmark("Applying Zebra widget", time); - } - } - }); - -})(jQuery); \ No newline at end of file diff --git a/sitestatic/jquery.tablesorter-2.3.11.min.js b/sitestatic/jquery.tablesorter-2.3.11.min.js deleted file mode 100644 index 20f808fa..00000000 --- a/sitestatic/jquery.tablesorter-2.3.11.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! -* TableSorter 2.3.11 - Client-side table sorting with ease! -* Minified using UglifyJS (http://jscompress.com/) -* Copyright (c) 2007 Christian Bach -*/ -!function($){$.extend({tablesorter:new function(){function log(a){if(typeof console!=="undefined"&&typeof console.log!=="undefined"){console.log(a)}else{alert(a)}}function benchmark(a,b){log(a+" ("+((new Date).getTime()-b.getTime())+"ms)")}function getElementText(a,b,c){if(!b){return""}var d=a.config,e=d.textExtraction,f="";if(e==="simple"){if(d.supportsTextContent){f=b.textContent}else{f=$(b).text()}}else{if(typeof e==="function"){f=e(b,a,c)}else if(typeof e==="object"&&e.hasOwnProperty(c)){f=e[c](b,a,c)}else{f=d.supportsTextContent?b.textContent:$(b).text()}}return $.trim(f)}function getParserById(a){var b,c=parsers.length;for(b=0;b'+this.innerHTML+"";if(i.onRenderHeader){i.onRenderHeader.apply(d,[a])}this.column=b[this.parentNode.rowIndex+"-"+this.cellIndex];this.order=formatSortingOrder(j.getData(d,c,"sortInitialOrder")||i.sortInitialOrder)?[1,0,2]:[0,1,2];this.count=-1;if(j.getData(d,c,"sorter")==="false"){this.sortDisabled=true}this.lockedOrder=false;f=j.getData(d,c,"lockedOrder")||false;if(typeof f!=="undefined"&&f!==false){this.order=this.lockedOrder=formatSortingOrder(f)?[1,1,1]:[0,0,0]}if(!this.sortDisabled){e=d.addClass(i.cssHeader)}i.headerList[a]=this;d.parent().addClass(i.cssHeader)});if(a.config.debug){benchmark("Built headers:",g);log(h)}return h}function isValueInArray(a,b){var c,d=b.length;for(c=0;c1&&d.length){for(g=0;g");$("tr:first td",a.tBodies[0]).each(function(){b.append($("").css("width",$(this).width()))});$(a).prepend(b)}}function updateHeaderSortCount(a,b){var c,d,e,f=a.config,g=b.length;for(c=0;c thead th",selectorRemove:"tr.remove-me",debug:false,headerList:[],empties:{},strings:{},parsers:[]};this.benchmark=benchmark;this.hasInitialized=false;this.construct=function(a){return this.each(function(){if(!this.tHead||this.tBodies.length===0){return}var b,c,d,e,f,g,h,i,j,k,l,m=$.metadata;this.config={};e=$.extend(true,this.config,$.tablesorter.defaults,a);if(e.debug){$.data(this,"startoveralltimer",new Date)}d=$(this).addClass(e.tableClass);$.data(this,"tablesorter",e);e.supportsTextContent=$("x")[0].textContent==="x";e.string={max:1,min:-1,"max+":1,"max-":-1,zero:0,none:0,"null":0,top:true,bottom:false};b=buildHeaders(this);e.parsers=buildParserCache(this,b);if(!e.delayInit){buildCache(this)}fixColumnWidth(this);b.bind("mousedown.tablesorter mouseup.tablesorter",function(a,m){if(a.type==="mousedown"){l=(new Date).getTime();return!e.cancelSelection}if(m!==true&&(new Date).getTime()-l>500){return false}if(e.delayInit&&!e.cache){buildCache(d[0])}if(!this.sortDisabled){d.trigger("sortStart",d[0]);c=$(this);h=!a[e.sortMultiSortKey];this.count=(this.count+1)%(e.sortReset?3:2);if(e.sortRestart){f=this;b.each(function(){if(this!==f&&(h||!$(this).is("."+e.cssDesc+",."+e.cssAsc))){this.count=-1}})}f=this.column;if(h){e.sortList=[];if(e.sortForce!==null){i=e.sortForce;for(g=0;g1){for(g=1;g1){for(g=1;g0){d.trigger("sorton",[e.sortList,{},!e.initWidgets])}else if(e.initWidgets){applyWidget(this)}this.hasInitialized=true;if(e.debug){$.tablesorter.benchmark("Overall initialization time",$.data(this,"startoveralltimer"))}d.trigger("tablesorter-initialized",this);if(typeof e.initialized==="function"){e.initialized(this)}})};this.sortText=function(a,b,c,d){if(b===c){return 0}var e=a.config,f=e.string[e.empties[d]||e.emptyTo],g=$.tablesorter.regex,h,i,j,k,l,m,n,o;if(b===""&&f!==0){return typeof f==="boolean"?f?-1:1:-f||-1}if(c===""&&f!==0){return typeof f==="boolean"?f?1:-1:f||1}if(typeof e.textSorter==="function"){return e.textSorter(b,c,a,d)}h=b.replace(g[0],"\\0$1\\0").replace(/\\0$/,"").replace(/^\\0/,"").split("\\0");j=c.replace(g[0],"\\0$1\\0").replace(/\\0$/,"").replace(/^\\0/,"").split("\\0");i=parseInt(b.match(g[2]),16)||h.length!==1&&b.match(g[1])&&Date.parse(b);k=parseInt(c.match(g[2]),16)||i&&c.match(g[1])&&Date.parse(c)||null;if(k){if(ik){return 1}}o=Math.max(h.length,j.length);for(n=0;nm){return 1}}return 0};this.sortTextDesc=function(a,b,c,d){if(b===c){return 0}var e=a.config,f=e.string[e.empties[d]||e.emptyTo];if(b===""&&f!==0){return typeof f==="boolean"?f?-1:1:f||1}if(c===""&&f!==0){return typeof f==="boolean"?f?1:-1:-f||-1}if(typeof e.textSorter==="function"){return e.textSorter(c,b,a,d)}return this.sortText(a,c,b)};this.getTextValue=function(a,b,c){if(b){var d,e=a.length,f=b+c;for(d=0;d1){e=0;c=b.find("tr:visible");b.addClass("tablesorter-hidden");c.each(function(){d=$(this);if(!k.test(this.className)){e++}f=e%2===0;d.removeClass(m[f?1:0]).addClass(m[f?0:1])});b.removeClass("tablesorter-hidden")}}if(j.debug){ts.benchmark("Applying Zebra widget",g)}}})}(jQuery) diff --git a/sitestatic/jquery.tablesorter-2.4.5.js b/sitestatic/jquery.tablesorter-2.4.5.js new file mode 100644 index 00000000..723cb91e --- /dev/null +++ b/sitestatic/jquery.tablesorter-2.4.5.js @@ -0,0 +1,1333 @@ +/*! +* TableSorter 2.4.5 - Client-side table sorting with ease! +* @requires jQuery v1.2.6+ +* +* Copyright (c) 2007 Christian Bach +* Examples and docs at: http://tablesorter.com +* Dual licensed under the MIT and GPL licenses: +* http://www.opensource.org/licenses/mit-license.php +* http://www.gnu.org/licenses/gpl.html +* +* @type jQuery +* @name tablesorter +* @cat Plugins/Tablesorter +* @author Christian Bach/christian.bach@polyester.se +* @contributor Rob Garrison/https://github.com/Mottie/tablesorter +*/ +/*jshint evil:true, browser:true, jquery:true, unused:false */ +/*global console:false, alert:false */ +!(function($) { + "use strict"; + $.extend({ + tablesorter: new function() { + + var ts = this; + + ts.version = "2.4.5"; + + ts.parsers = []; + ts.widgets = []; + ts.defaults = { + + // appearance + theme : 'default', // adds tablesorter-{theme} to the table for styling + widthFixed : false, // adds colgroup to fix widths of columns + showProcessing : false, // show an indeterminate timer icon in the header when the table is sorted or filtered. + + // functionality + cancelSelection : true, // prevent text selection in the header + dateFormat : 'mmddyyyy', // other options: "ddmmyyy" or "yyyymmdd" + sortMultiSortKey : 'shiftKey', // key used to select additional columns + usNumberFormat : true, // false for German "1.234.567,89" or French "1 234 567,89" + delayInit : false, // if false, the parsed table contents will not update until the first sort + + // sort options + headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc. + ignoreCase : true, // ignore case while sorting + sortForce : null, // column(s) first sorted; always applied + sortList : [], // Initial sort order; applied initially; updated when manually sorted + sortAppend : null, // column(s) sorted last; always applied + + sortInitialOrder : 'asc', // sort direction on first click + sortLocaleCompare: false, // replace equivalent character (accented characters) + sortReset : false, // third click on the header will reset column to default - unsorted + sortRestart : false, // restart sort to "sortInitialOrder" when clicking on previously unsorted columns + + emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero + stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero + textExtraction : 'simple', // text extraction method/function - function(node, table, cellIndex){} + textSorter : null, // use custom text sorter - function(a,b){ return a.sort(b); } // basic sort + + // widget options + widgets: [], // method to add widgets, e.g. widgets: ['zebra'] + widgetOptions : { + zebra : [ 'even', 'odd' ] // zebra widget alternating row class names + }, + initWidgets : true, // apply widgets on tablesorter initialization + + // callbacks + initialized : null, // function(table){}, + onRenderHeader : null, // function(index){}, + + // css class names + tableClass : 'tablesorter', + cssAsc : 'tablesorter-headerSortUp', + cssChildRow : 'tablesorter-childRow', // previously "expand-child" + cssDesc : 'tablesorter-headerSortDown', + cssHeader : 'tablesorter-header', + cssHeaderRow : 'tablesorter-headerRow', + cssIcon : 'tablesorter-icon', // if this class exists, a will be added to the header automatically + cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name + cssProcessing : 'tablesorter-processing', // processing icon applied to header during sort/filter + + // selectors + selectorHeaders : '> thead th, > thead td', + selectorSort : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort + selectorRemove : '.remove-me', + + // advanced + debug : false, + + // Internal variables + headerList: [], + empties: {}, + strings: {}, + parsers: [] + + // deprecated; but retained for backwards compatibility + // widgetZebra: { css: ["even", "odd"] } + + }; + + /* debuging utils */ + function log(s) { + if (typeof console !== "undefined" && typeof console.log !== "undefined") { + console.log(s); + } else { + alert(s); + } + } + + function benchmark(s, d) { + log(s + " (" + (new Date().getTime() - d.getTime()) + "ms)"); + } + + ts.benchmark = benchmark; + + function getElementText(table, node, cellIndex) { + if (!node) { return ""; } + var c = table.config, + t = c.textExtraction, text = ""; + if (t === "simple") { + if (c.supportsTextContent) { + text = node.textContent; // newer browsers support this + } else { + text = $(node).text(); + } + } else { + if (typeof(t) === "function") { + text = t(node, table, cellIndex); + } else if (typeof(t) === "object" && t.hasOwnProperty(cellIndex)) { + text = t[cellIndex](node, table, cellIndex); + } else { + text = c.supportsTextContent ? node.textContent : $(node).text(); + } + } + return $.trim(text); + } + + function detectParserForColumn(table, rows, rowIndex, cellIndex) { + var i, l = ts.parsers.length, + node = false, + nodeValue = '', + keepLooking = true; + while (nodeValue === '' && keepLooking) { + rowIndex++; + if (rows[rowIndex]) { + node = rows[rowIndex].cells[cellIndex]; + nodeValue = getElementText(table, node, cellIndex); + if (table.config.debug) { + log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': ' + nodeValue); + } + } else { + keepLooking = false; + } + } + for (i = 1; i < l; i++) { + if (ts.parsers[i].is(nodeValue, table, node)) { + return ts.parsers[i]; + } + } + // 0 is always the generic parser (text) + return ts.parsers[0]; + } + + function buildParserCache(table, $headers) { + var c = table.config, + tb = $(table.tBodies).filter(':not(.' + c.cssInfoBlock + ')'), + rows, list, l, i, h, ch, p, parsersDebug = ""; + if ( tb.length === 0) { return; } // In the case of empty tables + rows = tb[0].rows; + if (rows[0]) { + list = []; + l = rows[0].cells.length; + for (i = 0; i < l; i++) { + // tons of thanks to AnthonyM1229 for working out the following selector (issue #74) to make this work in IE8! + h = $headers.filter(':not([colspan])[data-column="' + i + '"]:last,[colspan="1"][data-column="' + i + '"]:last'); + ch = c.headers[i]; + // get column parser + p = ts.getParserById( ts.getData(h, ch, 'sorter') ); + // empty cells behaviour - keeping emptyToBottom for backwards compatibility + c.empties[i] = ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' ); + // text strings behaviour in numerical sorts + c.strings[i] = ts.getData(h, ch, 'string') || c.stringTo || 'max'; + if (!p) { + p = detectParserForColumn(table, rows, -1, i); + } + if (c.debug) { + parsersDebug += "column:" + i + "; parser:" + p.id + "; string:" + c.strings[i] + '; empty: ' + c.empties[i] + "\n"; + } + list.push(p); + } + } + if (c.debug) { + log(parsersDebug); + } + return list; + } + + /* utils */ + function buildCache(table) { + var b = table.tBodies, + tc = table.config, + totalRows, + totalCells, + parsers = tc.parsers, + t, i, j, k, c, cols, cacheTime; + tc.cache = {}; + if (tc.debug) { + cacheTime = new Date(); + } + // processing icon + if (tc.showProcessing) { + ts.isProcessing(table, true); + } + for (k = 0; k < b.length; k++) { + tc.cache[k] = { row: [], normalized: [] }; + // ignore tbodies with class name from css.cssInfoBlock + if (!$(b[k]).hasClass(tc.cssInfoBlock)) { + totalRows = (b[k] && b[k].rows.length) || 0; + totalCells = (b[k].rows[0] && b[k].rows[0].cells.length) || 0; + for (i = 0; i < totalRows; ++i) { + /** Add the table data to main data array */ + c = $(b[k].rows[i]); + cols = []; + // if this is a child row, add it to the last row's children and continue to the next row + if (c.hasClass(tc.cssChildRow)) { + tc.cache[k].row[tc.cache[k].row.length - 1] = tc.cache[k].row[tc.cache[k].row.length - 1].add(c); + // go to the next for loop + continue; + } + tc.cache[k].row.push(c); + for (j = 0; j < totalCells; ++j) { + t = getElementText(table, c[0].cells[j], j); + // allow parsing if the string is empty, previously parsing would change it to zero, + // in case the parser needs to extract data from the table cell attributes + cols.push( parsers[j].format(t, table, c[0].cells[j], j) ); + } + cols.push(tc.cache[k].normalized.length); // add position for rowCache + tc.cache[k].normalized.push(cols); + } + } + } + if (tc.showProcessing) { + ts.isProcessing(table); // remove processing icon + } + if (tc.debug) { + benchmark("Building cache for " + totalRows + " rows", cacheTime); + } + } + + // init flag (true) used by pager plugin to prevent widget application + function appendToTable(table, init) { + var c = table.config, + b = table.tBodies, + rows = [], + c2 = c.cache, + r, n, totalRows, checkCell, $bk, $tb, + i, j, k, l, pos, appendTime; + if (c.debug) { + appendTime = new Date(); + } + for (k = 0; k < b.length; k++) { + $bk = $(b[k]); + if (!$bk.hasClass(c.cssInfoBlock)) { + // get tbody + $tb = ts.processTbody(table, $bk, true); + r = c2[k].row; + n = c2[k].normalized; + totalRows = n.length; + checkCell = totalRows ? (n[0].length - 1) : 0; + for (i = 0; i < totalRows; i++) { + pos = n[i][checkCell]; + rows.push(r[pos]); + // removeRows used by the pager plugin + if (!c.appender || !c.removeRows) { + l = r[pos].length; + for (j = 0; j < l; j++) { + $tb.append(r[pos][j]); + } + } + } + // restore tbody + ts.processTbody(table, $tb, false); + } + } + if (c.appender) { + c.appender(table, rows); + } + if (c.debug) { + benchmark("Rebuilt table", appendTime); + } + // apply table widgets + if (!init) { ts.applyWidget(table); } + // trigger sortend + $(table).trigger("sortEnd", table); + } + + // computeTableHeaderCellIndexes from: + // http://www.javascripttoolbox.com/lib/table/examples.php + // http://www.javascripttoolbox.com/temp/table_cellindex.html + function computeThIndexes(t) { + var matrix = [], + lookup = {}, + trs = $(t).find('thead:eq(0) tr, tfoot tr'), + i, j, k, l, c, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol, matrixrow; + for (i = 0; i < trs.length; i++) { + cells = trs[i].cells; + for (j = 0; j < cells.length; j++) { + c = cells[j]; + rowIndex = c.parentNode.rowIndex; + cellId = rowIndex + "-" + c.cellIndex; + rowSpan = c.rowSpan || 1; + colSpan = c.colSpan || 1; + if (typeof(matrix[rowIndex]) === "undefined") { + matrix[rowIndex] = []; + } + // Find first available column in the first row + for (k = 0; k < matrix[rowIndex].length + 1; k++) { + if (typeof(matrix[rowIndex][k]) === "undefined") { + firstAvailCol = k; + break; + } + } + lookup[cellId] = firstAvailCol; + // add data-column + $(c).attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex + for (k = rowIndex; k < rowIndex + rowSpan; k++) { + if (typeof(matrix[k]) === "undefined") { + matrix[k] = []; + } + matrixrow = matrix[k]; + for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) { + matrixrow[l] = "x"; + } + } + } + } + return lookup; + } + + function formatSortingOrder(v) { + // look for "d" in "desc" order; return true + return (/^d/i.test(v) || v === 1); + } + + function buildHeaders(table) { + var header_index = computeThIndexes(table), ch, $t, + t, lock, time, $tableHeaders, c = table.config; + c.headerList = []; + if (c.debug) { + time = new Date(); + } + $tableHeaders = $(table).find(c.selectorHeaders).each(function(index) { + $t = $(this); + ch = c.headers[index]; + t = c.cssIcon ? '' : ''; // add icon if cssIcon option exists + this.innerHTML = '
    ' + this.innerHTML + t + '
    '; // faster than wrapInner + if (c.onRenderHeader) { c.onRenderHeader.apply($t, [index]); } + this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; + this.order = formatSortingOrder( ts.getData($t, ch, 'sortInitialOrder') || c.sortInitialOrder ) ? [1,0,2] : [0,1,2]; + this.count = -1; // set to -1 because clicking on the header automatically adds one + if (ts.getData($t, ch, 'sorter') === 'false') { + this.sortDisabled = true; + $t.addClass('sorter-false'); + } else { + $t.removeClass('sorter-false'); + } + this.lockedOrder = false; + lock = ts.getData($t, ch, 'lockedOrder') || false; + if (typeof(lock) !== 'undefined' && lock !== false) { + this.order = this.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0]; + } + $t.addClass( this.sortDisabled ? 'sorter-false' : c.cssHeader ); + // add cell to headerList + c.headerList[index] = this; + // add to parent in case there are multiple rows + $t.parent().addClass(c.cssHeaderRow); + }); + if (table.config.debug) { + benchmark("Built headers:", time); + log($tableHeaders); + } + return $tableHeaders; + } + + function setHeadersCss(table, $headers) { + var f, i, j, l, + c = table.config, + list = c.sortList, + css = [c.cssDesc, c.cssAsc], + // find the footer + $t = $(table).find('tfoot tr').children().removeClass(css.join(' ')); + // remove all header information + $headers.removeClass(css.join(' ')); + l = list.length; + for (i = 0; i < l; i++) { + // direction = 2 means reset! + if (list[i][1] !== 2) { + // multicolumn sorting updating - choose the :last in case there are nested columns + f = $headers.not('.sorter-false').filter('[data-column="' + list[i][0] + '"]' + (l === 1 ? ':last' : '') ); + if (f.length) { + for (j = 0; j < f.length; j++) { + if (!f[j].sortDisabled) { + f.eq(j).addClass(css[list[i][1]]); + // add sorted class to footer, if it exists + if ($t.length) { + $t.filter('[data-column="' + list[i][0] + '"]').eq(j).addClass(css[list[i][1]]); + } + } + } + } + } + } + } + + function fixColumnWidth(table) { + if (table.config.widthFixed && $(table).find('colgroup').length === 0) { + var colgroup = $('
    '), + overallWidth = $(table).width(); + $("tr:first td", table.tBodies[0]).each(function() { + colgroup.append($('').css('width', parseInt(($(this).width()/overallWidth)*1000, 10)/10 + '%')); + }); + $(table).prepend(colgroup); + } + } + + function updateHeaderSortCount(table, list) { + var s, o, c = table.config, + l = c.headerList.length, + sl = list || c.sortList; + c.sortList = []; + $.each(sl, function(i,v){ + // ensure all sortList values are numeric - fixes #127 + s = [ parseInt(v[0], 10), parseInt(v[1], 10) ]; + // make sure header exists + o = c.headerList[s[0]]; + if (o) { // prevents error if sorton array is wrong + c.sortList.push(s); + o.count = s[1] % (c.sortReset ? 3 : 2); + } + }); + } + + function getCachedSortType(parsers, i) { + return (parsers && parsers[i]) ? parsers[i].type || '' : ''; + } + + // sort multiple columns + function multisort(table) { + var dynamicExp, sortWrapper, col, mx = 0, dir = 0, tc = table.config, + sortList = tc.sortList, l = sortList.length, bl = table.tBodies.length, + sortTime, i, j, k, c, cache, lc, s, e, order, orgOrderCol; + if (tc.debug) { sortTime = new Date(); } + for (k = 0; k < bl; k++) { + dynamicExp = "sortWrapper = function(a,b) {"; + cache = tc.cache[k]; + lc = cache.normalized.length; + for (i = 0; i < l; i++) { + c = sortList[i][0]; + order = sortList[i][1]; + // fallback to natural sort since it is more robust + s = /n/i.test(getCachedSortType(tc.parsers, c)) ? "Numeric" : "Text"; + s += order === 0 ? "" : "Desc"; + e = "e" + i; + // get max column value (ignore sign) + if (/Numeric/.test(s) && tc.strings[c]) { + for (j = 0; j < lc; j++) { + col = Math.abs(parseFloat(cache.normalized[j][c])); + mx = Math.max( mx, isNaN(col) ? 0 : col ); + } + // sort strings in numerical columns + if (typeof(tc.string[tc.strings[c]]) === 'boolean') { + dir = (order === 0 ? 1 : -1) * (tc.string[tc.strings[c]] ? -1 : 1); + } else { + dir = (tc.strings[c]) ? tc.string[tc.strings[c]] || 0 : 0; + } + } + dynamicExp += "var " + e + " = $.tablesorter.sort" + s + "(table,a[" + c + "],b[" + c + "]," + c + "," + mx + "," + dir + "); "; + dynamicExp += "if (" + e + ") { return " + e + "; } "; + dynamicExp += "else { "; + } + // if value is the same keep orignal order + orgOrderCol = (cache.normalized && cache.normalized[0]) ? cache.normalized[0].length - 1 : 0; + dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];"; + for (i=0; i < l; i++) { + dynamicExp += "}; "; + } + dynamicExp += "return 0; "; + dynamicExp += "}; "; + cache.normalized.sort(eval(dynamicExp)); // sort using eval expression + } + if (tc.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time", sortTime); } + } + + function resortComplete($table, callback){ + var t = $table[0]; + $table.trigger('updateComplete'); + if (typeof callback === "function") { + callback(t); + } + } + + function checkResort($table, flag, callback) { + if (flag !== false) { + $table.trigger("sorton", [$table[0].config.sortList, function(){ + resortComplete($table, callback); + }]); + } else { + resortComplete($table, callback); + } + } + + /* public methods */ + ts.construct = function(settings) { + return this.each(function() { + // if no thead or tbody, or tablesorter is already present, quit + if (!this.tHead || this.tBodies.length === 0 || this.hasInitialized === true) { return; } + // declare + var $headers, $cell, $this = $(this), + c, i, j, k = '', a, s, o, downTime, + m = $.metadata; + // initialization flag + this.hasInitialized = false; + // new blank config object + this.config = {}; + // merge and extend + c = $.extend(true, this.config, ts.defaults, settings); + // save the settings where they read + $.data(this, "tablesorter", c); + if (c.debug) { $.data( this, 'startoveralltimer', new Date()); } + // constants + c.supportsTextContent = $('x')[0].textContent === 'x'; + c.supportsDataObject = parseFloat($.fn.jquery) >= 1.4; + // digit sort text location; keeping max+/- for backwards compatibility + c.string = { 'max': 1, 'min': -1, 'max+': 1, 'max-': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false }; + // add table theme class only if there isn't already one there + if (!/tablesorter\-/.test($this.attr('class'))) { + k = (c.theme !== '' ? ' tablesorter-' + c.theme : ''); + } + $this.addClass(c.tableClass + k); + // build headers + $headers = buildHeaders(this); + // try to auto detect column type, and store in tables config + c.parsers = buildParserCache(this, $headers); + // build the cache for the tbody cells + // delayInit will delay building the cache until the user starts a sort + if (!c.delayInit) { buildCache(this); } + // apply event handling to headers + // this is to big, perhaps break it out? + $headers + // http://stackoverflow.com/questions/5312849/jquery-find-self + .find('*').andSelf().filter(c.selectorSort) + .unbind('mousedown.tablesorter mouseup.tablesorter') + .bind('mousedown.tablesorter mouseup.tablesorter', function(e, external) { + // jQuery v1.2.6 doesn't have closest() + var $cell = this.tagName.match('TH|TD') ? $(this) : $(this).parents('th, td').filter(':last'), cell = $cell[0]; + // only recognize left clicks + if ((e.which || e.button) !== 1) { return false; } + // set timer on mousedown + if (e.type === 'mousedown') { + downTime = new Date().getTime(); + return e.target.tagName === "INPUT" ? '' : !c.cancelSelection; + } + // ignore long clicks (prevents resizable widget from initializing a sort) + if (external !== true && (new Date().getTime() - downTime > 250)) { return false; } + if (c.delayInit && !c.cache) { buildCache($this[0]); } + if (!cell.sortDisabled) { + // Only call sortStart if sorting is enabled + $this.trigger("sortStart", $this[0]); + // store exp, for speed + // $cell = $(this); + k = !e[c.sortMultiSortKey]; + // get current column sort order + cell.count = (cell.count + 1) % (c.sortReset ? 3 : 2); + // reset all sorts on non-current column - issue #30 + if (c.sortRestart) { + i = cell; + $headers.each(function() { + // only reset counts on columns that weren't just clicked on and if not included in a multisort + if (this !== i && (k || !$(this).is('.' + c.cssDesc + ',.' + c.cssAsc))) { + this.count = -1; + } + }); + } + // get current column index + i = cell.column; + // user only wants to sort on one column + if (k) { + // flush the sort list + c.sortList = []; + if (c.sortForce !== null) { + a = c.sortForce; + for (j = 0; j < a.length; j++) { + if (a[j][0] !== i) { + c.sortList.push(a[j]); + } + } + } + // add column to sort list + o = cell.order[cell.count]; + if (o < 2) { + c.sortList.push([i, o]); + // add other columns if header spans across multiple + if (cell.colSpan > 1) { + for (j = 1; j < cell.colSpan; j++) { + c.sortList.push([i + j, o]); + } + } + } + // multi column sorting + } else { + // get rid of the sortAppend before adding more - fixes issue #115 + if (c.sortAppend && c.sortList.length > 1) { + if (ts.isValueInArray(c.sortAppend[0][0], c.sortList)) { + c.sortList.pop(); + } + } + // the user has clicked on an already sorted column + if (ts.isValueInArray(i, c.sortList)) { + // reverse the sorting direction for all tables + for (j = 0; j < c.sortList.length; j++) { + s = c.sortList[j]; + o = c.headerList[s[0]]; + if (s[0] === i) { + s[1] = o.order[o.count]; + if (s[1] === 2) { + c.sortList.splice(j,1); + o.count = -1; + } + } + } + } else { + // add column to sort list array + o = cell.order[cell.count]; + if (o < 2) { + c.sortList.push([i, o]); + // add other columns if header spans across multiple + if (cell.colSpan > 1) { + for (j = 1; j < cell.colSpan; j++) { + c.sortList.push([i + j, o]); + } + } + } + } + } + if (c.sortAppend !== null) { + a = c.sortAppend; + for (j = 0; j < a.length; j++) { + if (a[j][0] !== i) { + c.sortList.push(a[j]); + } + } + } + // sortBegin event triggered immediately before the sort + $this.trigger("sortBegin", $this[0]); + // setTimeout needed so the processing icon shows up + setTimeout(function(){ + // set css for headers + setHeadersCss($this[0], $headers); + multisort($this[0]); + appendToTable($this[0]); + }, 1); + } + }); + if (c.cancelSelection) { + // cancel selection + $headers.each(function() { + this.onselectstart = function() { + return false; + }; + }); + } + // apply easy methods that trigger binded events + $this + .unbind('update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave') + .bind("update", function(e, resort, callback) { + // remove rows/elements before update + $(c.selectorRemove, this).remove(); + // rebuild parsers + c.parsers = buildParserCache(this, $headers); + // rebuild the cache map + buildCache(this); + checkResort($this, resort, callback); + }) + .bind("updateCell", function(e, cell, resort, callback) { + // get position from the dom + var l, row, icell, + t = this, $tb = $(this).find('tbody'), + // update cache - format: function(s, table, cell, cellIndex) + // no closest in jQuery v1.2.6 - tbdy = $tb.index( $(cell).closest('tbody') ),$row = $(cell).closest('tr'); + tbdy = $tb.index( $(cell).parents('tbody').filter(':last') ), + $row = $(cell).parents('tr').filter(':last'); + // tbody may not exist if update is initialized while tbody is removed for processing + if ($tb.length && tbdy >= 0) { + row = $tb.eq(tbdy).find('tr').index( $row ); + icell = cell.cellIndex; + l = t.config.cache[tbdy].normalized[row].length - 1; + t.config.cache[tbdy].row[t.config.cache[tbdy].normalized[row][l]] = $row; + t.config.cache[tbdy].normalized[row][icell] = c.parsers[icell].format( getElementText(t, cell, icell), t, cell, icell ); + checkResort($this, resort, callback); + } + }) + .bind("addRows", function(e, $row, resort, callback) { + var i, rows = $row.filter('tr').length, + dat = [], l = $row[0].cells.length, t = this, + tbdy = $(this).find('tbody').index( $row.closest('tbody') ); + // add each row + for (i = 0; i < rows; i++) { + // add each cell + for (j = 0; j < l; j++) { + dat[j] = c.parsers[j].format( getElementText(t, $row[i].cells[j], j), t, $row[i].cells[j], j ); + } + // add the row index to the end + dat.push(c.cache[tbdy].row.length); + // update cache + c.cache[tbdy].row.push([$row[i]]); + c.cache[tbdy].normalized.push(dat); + dat = []; + } + // resort using current settings + checkResort($this, resort, callback); + }) + .bind("sorton", function(e, list, callback, init) { + $(this).trigger("sortStart", this); + // update header count index + updateHeaderSortCount(this, list); + // set css for headers + setHeadersCss(this, $headers); + // sort the table and append it to the dom + multisort(this); + appendToTable(this, init); + if (typeof callback === "function") { + callback(this); + } + }) + .bind("appendCache", function(e, callback, init) { + appendToTable(this, init); + if (typeof callback === "function") { + callback(this); + } + }) + .bind("applyWidgetId", function(e, id) { + ts.getWidgetById(id).format(this, c, c.widgetOptions); + }) + .bind("applyWidgets", function(e, init) { + // apply widgets + ts.applyWidget(this, init); + }) + .bind("refreshWidgets", function(e, all, dontapply){ + ts.refreshWidgets(this, all, dontapply); + }) + .bind("destroy", function(e, c, cb){ + ts.destroy(this, c, cb); + }); + + // get sort list from jQuery data or metadata + // in jQuery < 1.4, an error occurs when calling $this.data() + if (c.supportsDataObject && typeof $this.data().sortlist !== 'undefined') { + c.sortList = $this.data().sortlist; + } else if (m && ($this.metadata() && $this.metadata().sortlist)) { + c.sortList = $this.metadata().sortlist; + } + // apply widget init code + ts.applyWidget(this, true); + // if user has supplied a sort list to constructor + if (c.sortList.length > 0) { + $this.trigger("sorton", [c.sortList, {}, !c.initWidgets]); + } else if (c.initWidgets) { + // apply widget format + ts.applyWidget(this); + } + + // fixate columns if the users supplies the fixedWidth option + // do this after theme has been applied + fixColumnWidth(this); + + // show processesing icon + if (c.showProcessing) { + $this + .unbind('sortBegin sortEnd') + .bind('sortBegin sortEnd', function(e) { + ts.isProcessing($this[0], e.type === 'sortBegin'); + }); + } + + // initialized + this.hasInitialized = true; + if (c.debug) { + ts.benchmark("Overall initialization time", $.data( this, 'startoveralltimer')); + } + $this.trigger('tablesorter-initialized', this); + if (typeof c.initialized === 'function') { c.initialized(this); } + }); + }; + + // *** Process table *** + // add processing indicator + ts.isProcessing = function(table, toggle, $ths) { + var c = table.config, + // default to all headers + $h = $ths || $(table).find('.' + c.cssHeader); + if (toggle) { + if (c.sortList.length > 0) { + // get headers from the sortList + $h = $h.filter(function(){ + // get data-column from attr to keep compatibility with jQuery 1.2.6 + return this.sortDisabled ? false : ts.isValueInArray( parseFloat($(this).attr('data-column')), c.sortList); + }); + } + $h.addClass(c.cssProcessing); + } else { + $h.removeClass(c.cssProcessing); + } + }; + + // detach tbody but save the position + // don't use tbody because there are portions that look for a tbody index (updateCell) + ts.processTbody = function(table, $tb, getIt){ + var t, holdr; + if (getIt) { + $tb.before(''); + holdr = ($.fn.detach) ? $tb.detach() : $tb.remove(); + return holdr; + } + holdr = $(table).find('span.tablesorter-savemyplace'); + $tb.insertAfter( holdr ); + holdr.remove(); + }; + + ts.clearTableBody = function(table) { + $(table.tBodies).filter(':not(.' + table.config.cssInfoBlock + ')').empty(); + }; + + ts.destroy = function(table, removeClasses, callback){ + var $t = $(table), c = table.config, + $h = $t.find('thead:first'); + // clear flag in case the plugin is initialized again + table.hasInitialized = false; + // remove widget added rows + $h.find('tr:not(.' + c.cssHeaderRow + ')').remove(); + // remove resizer widget stuff + $h.find('.tablesorter-resizer').remove(); + // remove all widgets + ts.refreshWidgets(table, true, true); + // disable tablesorter + $t + .removeData('tablesorter') + .unbind('update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave') + .find('.' + c.cssHeader) + .unbind('click mousedown mousemove mouseup') + .removeClass(c.cssHeader + ' ' + c.cssAsc + ' ' + c.cssDesc) + .find('.tablesorter-header-inner').each(function(){ + if (c.cssIcon !== '') { $(this).find('.' + c.cssIcon).remove(); } + $(this).replaceWith( $(this).contents() ); + }); + if (removeClasses !== false) { + $t.removeClass(c.tableClass); + } + if (typeof callback === 'function') { + callback(table); + } + }; + + // *** sort functions *** + // regex used in natural sort + ts.regex = [ + /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi, // chunk/tokenize numbers & letters + /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/, //date + /^0x[0-9a-f]+$/i // hex + ]; + + // Natural sort - https://github.com/overset/javascript-natural-sort + ts.sortText = function(table, a, b, col) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ], + r = ts.regex, xN, xD, yN, yD, xF, yF, i, mx; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } + if (typeof c.textSorter === 'function') { return c.textSorter(a, b, table, col); } + // chunk/tokenize + xN = a.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); + yN = b.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); + // numeric, hex or date detection + xD = parseInt(a.match(r[2]),16) || (xN.length !== 1 && a.match(r[1]) && Date.parse(a)); + yD = parseInt(b.match(r[2]),16) || (xD && b.match(r[1]) && Date.parse(b)) || null; + // first try and sort Hex codes or Dates + if (yD) { + if ( xD < yD ) { return -1; } + if ( xD > yD ) { return 1; } + } + mx = Math.max(xN.length, yN.length); + // natural sorting through split numeric strings and default strings + for (i = 0; i < mx; i++) { + // find floats not starting with '0', string or 0 if not defined + xF = isNaN(xN[i]) ? xN[i] || 0 : parseFloat(xN[i]) || 0; + yF = isNaN(yN[i]) ? yN[i] || 0 : parseFloat(yN[i]) || 0; + // handle numeric vs string comparison - number < string - (Kyle Adams) + if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; } + // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' + if (typeof xF !== typeof yF) { + xF += ''; + yF += ''; + } + if (xF < yF) { return -1; } + if (xF > yF) { return 1; } + } + return 0; + }; + + ts.sortTextDesc = function(table, a, b, col) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } + if (typeof c.textSorter === 'function') { return c.textSorter(b, a, table, col); } + return ts.sortText(table, b, a); + }; + + // return text string value by adding up ascii value + // so the text is somewhat sorted when using a digital sort + // this is NOT an alphanumeric sort + ts.getTextValue = function(a, mx, d) { + if (mx) { + // make sure the text value is greater than the max numerical value (mx) + var i, l = a.length, n = mx + d; + for (i = 0; i < l; i++) { + n += a.charCodeAt(i); + } + return d * n; + } + return 0; + }; + + ts.sortNumeric = function(table, a, b, col, mx, d) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } + if (isNaN(a)) { a = ts.getTextValue(a, mx, d); } + if (isNaN(b)) { b = ts.getTextValue(b, mx, d); } + return a - b; + }; + + ts.sortNumericDesc = function(table, a, b, col, mx, d) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } + if (isNaN(a)) { a = ts.getTextValue(a, mx, d); } + if (isNaN(b)) { b = ts.getTextValue(b, mx, d); } + return b - a; + }; + + // used when replacing accented characters during sorting + ts.characterEquivalents = { + "a" : "\u00e1\u00e0\u00e2\u00e3\u00e4", // áàâãä + "A" : "\u00c1\u00c0\u00c2\u00c3\u00c4", // ÁÀÂÃÄ + "c" : "\u00e7", // ç + "C" : "\u00c7", // Ç + "e" : "\u00e9\u00e8\u00ea\u00eb", // éèêë + "E" : "\u00c9\u00c8\u00ca\u00cb", // ÉÈÊË + "i" : "\u00ed\u00ec\u0130\u00ee\u00ef", // íìİîï + "I" : "\u00cd\u00cc\u0130\u00ce\u00cf", // ÍÌİÎÏ + "o" : "\u00f3\u00f2\u00f4\u00f5\u00f6", // óòôõö + "O" : "\u00d3\u00d2\u00d4\u00d5\u00d6", // ÓÒÔÕÖ + "S" : "\u00df", // ß + "u" : "\u00fa\u00f9\u00fb\u00fc", // úùûü + "U" : "\u00da\u00d9\u00db\u00dc" // ÚÙÛÜ + }; + ts.replaceAccents = function(s) { + var a, acc = '[', eq = ts.characterEquivalents; + if (!ts.characterRegex) { + ts.characterRegexArray = {}; + for (a in eq) { + if (typeof a === 'string') { + acc += eq[a]; + ts.characterRegexArray[a] = new RegExp('[' + eq[a] + ']', 'g'); + } + } + ts.characterRegex = new RegExp(acc + ']'); + } + if (ts.characterRegex.test(s)) { + for (a in eq) { + if (typeof a === 'string') { + s = s.replace( ts.characterRegexArray[a], a ); + } + } + } + return s; + }; + + // *** utilities *** + ts.isValueInArray = function(v, a) { + var i, l = a.length; + for (i = 0; i < l; i++) { + if (a[i][0] === v) { + return true; + } + } + return false; + }; + + ts.addParser = function(parser) { + var i, l = ts.parsers.length, a = true; + for (i = 0; i < l; i++) { + if (ts.parsers[i].id.toLowerCase() === parser.id.toLowerCase()) { + a = false; + } + } + if (a) { + ts.parsers.push(parser); + } + }; + + ts.getParserById = function(name) { + var i, l = ts.parsers.length; + for (i = 0; i < l; i++) { + if (ts.parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) { + return ts.parsers[i]; + } + } + return false; + }; + + ts.addWidget = function(widget) { + ts.widgets.push(widget); + }; + + ts.getWidgetById = function(name) { + var i, w, l = ts.widgets.length; + for (i = 0; i < l; i++) { + w = ts.widgets[i]; + if (w && w.hasOwnProperty('id') && w.id.toLowerCase() === name.toLowerCase()) { + return w; + } + } + }; + + ts.applyWidget = function(table, init) { + var c = table.config, + wo = c.widgetOptions, + ws = c.widgets.sort().reverse(), // ensure that widgets are always applied in a certain order + time, i, w, l = ws.length; + // make zebra last + i = $.inArray('zebra', c.widgets); + if (i >= 0) { + c.widgets.splice(i,1); + c.widgets.push('zebra'); + } + if (c.debug) { + time = new Date(); + } + // add selected widgets + for (i = 0; i < l; i++) { + w = ts.getWidgetById(ws[i]); + if ( w ) { + if (init === true && w.hasOwnProperty('init')) { + w.init(table, w, c, wo); + } else if (!init && w.hasOwnProperty('format')) { + w.format(table, c, wo); + } + } + } + if (c.debug) { + benchmark("Completed " + (init === true ? "initializing" : "applying") + " widgets", time); + } + }; + + ts.refreshWidgets = function(table, doAll, dontapply) { + var i, c = table.config, + cw = c.widgets, + w = ts.widgets, l = w.length; + // remove previous widgets + for (i = 0; i < l; i++){ + if ( w[i] && w[i].id && (doAll || $.inArray( w[i].id, cw ) < 0) ) { + if (c.debug) { log( 'removing ' + w[i].id ); } + if (w[i].hasOwnProperty('remove')) { w[i].remove(table, c, c.widgetOptions); } + } + } + if (dontapply !== true) { + ts.applyWidget(table, doAll); + } + }; + + // get sorter, string, empty, etc options for each column from + // jQuery data, metadata, header option or header class name ("sorter-false") + // priority = jQuery data > meta > headers option > header class name + ts.getData = function(h, ch, key) { + var val = '', $h = $(h), m, cl; + if (!$h.length) { return ''; } + m = $.metadata ? $h.metadata() : false; + cl = ' ' + ($h.attr('class') || ''); + if (typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined'){ + // "data-lockedOrder" is assigned to "lockedorder"; but "data-locked-order" is assigned to "lockedOrder" + // "data-sort-initial-order" is assigned to "sortInitialOrder" + val += $h.data(key) || $h.data(key.toLowerCase()); + } else if (m && typeof m[key] !== 'undefined') { + val += m[key]; + } else if (ch && typeof ch[key] !== 'undefined') { + val += ch[key]; + } else if (cl !== ' ' && cl.match(' ' + key + '-')) { + // include sorter class name "sorter-text", etc + val = cl.match( new RegExp(' ' + key + '-(\\w+)') )[1] || ''; + } + return $.trim(val); + }; + + ts.formatFloat = function(s, table) { + if (typeof(s) !== 'string' || s === '') { return s; } + if (table.config.usNumberFormat !== false) { + // US Format - 1,234,567.89 -> 1234567.89 + s = s.replace(/,/g,''); + } else { + // German Format = 1.234.567,89 -> 1234567.89 + // French Format = 1 234 567,89 -> 1234567.89 + s = s.replace(/[\s|\.]/g,'').replace(/,/g,'.'); + } + if(/^\s*\([.\d]+\)/.test(s)) { + // make (#) into a negative number -> (10) = -10 + s = s.replace(/^\s*\(/,'-').replace(/\)/,''); + } + var i = parseFloat(s); + // return the text instead of zero + return isNaN(i) ? $.trim(s) : i; + }; + + ts.isDigit = function(s) { + // replace all unwanted chars and match + return isNaN(s) ? (/^[\-+(]?\d+[)]?$/).test(s.toString().replace(/[,.'\s]/g, '')) : true; + }; + + }() + }); + + // make shortcut + var ts = $.tablesorter; + + // extend plugin scope + $.fn.extend({ + tablesorter: ts.construct + }); + + // add default parsers + ts.addParser({ + id: "text", + is: function(s, table, node) { + return true; + }, + format: function(s, table, cell, cellIndex) { + var c = table.config; + s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s ); + return c.sortLocaleCompare ? ts.replaceAccents(s) : s; + }, + type: "text" + }); + + ts.addParser({ + id: "currency", + is: function(s) { + return (/^\(?[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+/).test(s); // £$€¤¥¢ + }, + format: function(s, table) { + return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "ipAddress", + is: function(s) { + return (/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/).test(s); + }, + format: function(s, table) { + var i, a = s.split("."), + r = "", + l = a.length; + for (i = 0; i < l; i++) { + r += ("00" + a[i]).slice(-3); + } + return ts.formatFloat(r, table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "url", + is: function(s) { + return (/^(https?|ftp|file):\/\//).test(s); + }, + format: function(s) { + return $.trim(s.replace(/(https?|ftp|file):\/\//, '')); + }, + type: "text" + }); + + ts.addParser({ + id: "isoDate", + is: function(s) { + return (/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/).test(s); + }, + format: function(s, table) { + return ts.formatFloat((s !== "") ? (new Date(s.replace(/-/g, "/")).getTime() || "") : "", table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "percent", + is: function(s) { + return (/\d%\)?$/).test(s); + }, + format: function(s, table) { + return ts.formatFloat(s.replace(/%/g, ""), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "usLongDate", + is: function(s) { + return (/^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4}|'?\d{2})\s+(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i).test(s); + }, + format: function(s, table) { + return ts.formatFloat( (new Date(s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ''), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "shortDate", // "mmddyyyy", "ddmmyyyy" or "yyyymmdd" + is: function(s) { + // testing for ####-##-#### - so it's not perfect + return (/^(\d{2}|\d{4})[\/\-\,\.\s+]\d{2}[\/\-\.\,\s+](\d{2}|\d{4})$/).test(s); + }, + format: function(s, table, cell, cellIndex) { + var c = table.config, ci = c.headerList[cellIndex], + format = ci.shortDateFormat; + if (typeof format === 'undefined') { + // cache header formatting so it doesn't getData for every cell in the column + format = ci.shortDateFormat = ts.getData( ci, c.headers[cellIndex], 'dateFormat') || c.dateFormat; + } + s = s.replace(/\s+/g," ").replace(/[\-|\.|\,]/g, "/"); + if (format === "mmddyyyy") { + s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$1/$2"); + } else if (format === "ddmmyyyy") { + s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$2/$1"); + } else if (format === "yyyymmdd") { + s = s.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/, "$1/$2/$3"); + } + return ts.formatFloat( (new Date(s).getTime() || ''), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "time", + is: function(s) { + return (/^(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i).test(s); + }, + format: function(s, table) { + return ts.formatFloat( (new Date("2000/01/01 " + s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ""), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "digit", + is: function(s) { + return ts.isDigit(s); + }, + format: function(s, table) { + return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "metadata", + is: function(s) { + return false; + }, + format: function(s, table, cell) { + var c = table.config, + p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; + return $(cell).metadata()[p]; + }, + type: "numeric" + }); + + // add default widgets + ts.addWidget({ + id: "zebra", + format: function(table, c, wo) { + var $tb, $tv, $tr, row, even, time, k, l, + child = new RegExp(c.cssChildRow, 'i'), + b = $(table).children('tbody:not(.' + c.cssInfoBlock + ')'); + if (c.debug) { + time = new Date(); + } + for (k = 0; k < b.length; k++ ) { + // loop through the visible rows + $tb = $(b[k]); + l = $tb.children('tr').length; + if (l > 1) { + row = 0; + $tv = $tb.children('tr:visible'); + // revered back to using jQuery each - strangely it's the fastest method + $tv.each(function(){ + $tr = $(this); + // style children rows the same way the parent row was styled + if (!child.test(this.className)) { row++; } + even = (row % 2 === 0); + $tr.removeClass(wo.zebra[even ? 1 : 0]).addClass(wo.zebra[even ? 0 : 1]); + }); + } + } + if (c.debug) { + ts.benchmark("Applying Zebra widget", time); + } + }, + remove: function(table, c, wo){ + var k, $tb, + b = $(table).children('tbody:not(.' + c.cssInfoBlock + ')'), + rmv = (c.widgetOptions.zebra || [ "even", "odd" ]).join(' '); + for (k = 0; k < b.length; k++ ){ + $tb = $.tablesorter.processTbody(table, $(b[k]), true); // remove tbody + $tb.children().removeClass(rmv); + $.tablesorter.processTbody(table, $tb, false); // restore tbody + } + } + }); + +})(jQuery); \ No newline at end of file diff --git a/sitestatic/jquery.tablesorter-2.4.5.min.js b/sitestatic/jquery.tablesorter-2.4.5.min.js new file mode 100644 index 00000000..a52ce35f --- /dev/null +++ b/sitestatic/jquery.tablesorter-2.4.5.min.js @@ -0,0 +1,6 @@ +/*! +* TableSorter 2.4.5 - Client-side table sorting with ease! +* Minified using UglifyJS (http://jscompress.com/) +* Copyright (c) 2007 Christian Bach +*/ +!function($){"use strict";$.extend({tablesorter:new function(){function log(a){if(typeof console!=="undefined"&&typeof console.log!=="undefined"){console.log(a)}else{alert(a)}}function benchmark(a,b){log(a+" ("+((new Date).getTime()-b.getTime())+"ms)")}function getElementText(a,b,c){if(!b){return""}var d=a.config,e=d.textExtraction,f="";if(e==="simple"){if(d.supportsTextContent){f=b.textContent}else{f=$(b).text()}}else{if(typeof e==="function"){f=e(b,a,c)}else if(typeof e==="object"&&e.hasOwnProperty(c)){f=e[c](b,a,c)}else{f=d.supportsTextContent?b.textContent:$(b).text()}}return $.trim(f)}function detectParserForColumn(a,b,c,d){var e,f=ts.parsers.length,g=false,h="",i=true;while(h===""&&i){c++;if(b[c]){g=b[c].cells[d];h=getElementText(a,g,d);if(a.config.debug){log("Checking if value was empty on row "+c+", column: "+d+": "+h)}}else{i=false}}for(e=1;e':"";this.innerHTML='
    '+this.innerHTML+e+"
    ";if(i.onRenderHeader){i.onRenderHeader.apply(d,[a])}this.column=b[this.parentNode.rowIndex+"-"+this.cellIndex];this.order=formatSortingOrder(ts.getData(d,c,"sortInitialOrder")||i.sortInitialOrder)?[1,0,2]:[0,1,2];this.count=-1;if(ts.getData(d,c,"sorter")==="false"){this.sortDisabled=true;d.addClass("sorter-false")}else{d.removeClass("sorter-false")}this.lockedOrder=false;f=ts.getData(d,c,"lockedOrder")||false;if(typeof f!=="undefined"&&f!==false){this.order=this.lockedOrder=formatSortingOrder(f)?[1,1,1]:[0,0,0]}d.addClass(this.sortDisabled?"sorter-false":i.cssHeader);i.headerList[a]=this;d.parent().addClass(i.cssHeaderRow)});if(a.config.debug){benchmark("Built headers:",g);log(h)}return h}function setHeadersCss(a,b){var c,d,e,f,g=a.config,h=g.sortList,i=[g.cssDesc,g.cssAsc],j=$(a).find("tfoot tr").children().removeClass(i.join(" "));b.removeClass(i.join(" "));f=h.length;for(d=0;d"),c=$(a).width();$("tr:first td",a.tBodies[0]).each(function(){b.append($("").css("width",parseInt($(this).width()/c*1e3,10)/10+"%"))});$(a).prepend(b)}}function updateHeaderSortCount(a,b){var c,d,e=a.config,f=e.headerList.length,g=b||e.sortList;e.sortList=[];$.each(g,function(a,b){c=[parseInt(b[0],10),parseInt(b[1],10)];d=e.headerList[c[0]];if(d){e.sortList.push(c);d.count=c[1]%(e.sortReset?3:2)}})}function getCachedSortType(a,b){return a&&a[b]?a[b].type||"":""}function multisort(table){var dynamicExp,sortWrapper,col,mx=0,dir=0,tc=table.config,sortList=tc.sortList,l=sortList.length,bl=table.tBodies.length,sortTime,i,j,k,c,cache,lc,s,e,order,orgOrderCol;if(tc.debug){sortTime=new Date}for(k=0;k thead th, > thead td",selectorSort:"th, td",selectorRemove:".remove-me",debug:false,headerList:[],empties:{},strings:{},parsers:[]};ts.benchmark=benchmark;ts.construct=function(a){return this.each(function(){if(!this.tHead||this.tBodies.length===0||this.hasInitialized===true){return}var b,c,d=$(this),e,f,g,h="",i,j,k,l,m=$.metadata;this.hasInitialized=false;this.config={};e=$.extend(true,this.config,ts.defaults,a);$.data(this,"tablesorter",e);if(e.debug){$.data(this,"startoveralltimer",new Date)}e.supportsTextContent=$("x")[0].textContent==="x";e.supportsDataObject=parseFloat($.fn.jquery)>=1.4;e.string={max:1,min:-1,"max+":1,"max-":-1,zero:0,none:0,"null":0,top:true,bottom:false};if(!/tablesorter\-/.test(d.attr("class"))){h=e.theme!==""?" tablesorter-"+e.theme:""}d.addClass(e.tableClass+h);b=buildHeaders(this);e.parsers=buildParserCache(this,b);if(!e.delayInit){buildCache(this)}b.find("*").andSelf().filter(e.selectorSort).unbind("mousedown.tablesorter mouseup.tablesorter").bind("mousedown.tablesorter mouseup.tablesorter",function(a,c){var m=this.tagName.match("TH|TD")?$(this):$(this).parents("th, td").filter(":last"),n=m[0];if((a.which||a.button)!==1){return false}if(a.type==="mousedown"){l=(new Date).getTime();return a.target.tagName==="INPUT"?"":!e.cancelSelection}if(c!==true&&(new Date).getTime()-l>250){return false}if(e.delayInit&&!e.cache){buildCache(d[0])}if(!n.sortDisabled){d.trigger("sortStart",d[0]);h=!a[e.sortMultiSortKey];n.count=(n.count+1)%(e.sortReset?3:2);if(e.sortRestart){f=n;b.each(function(){if(this!==f&&(h||!$(this).is("."+e.cssDesc+",."+e.cssAsc))){this.count=-1}})}f=n.column;if(h){e.sortList=[];if(e.sortForce!==null){i=e.sortForce;for(g=0;g1){for(g=1;g1){if(ts.isValueInArray(e.sortAppend[0][0],e.sortList)){e.sortList.pop()}}if(ts.isValueInArray(f,e.sortList)){for(g=0;g1){for(g=1;g=0){h=k.eq(l).find("tr").index(m);i=b.cellIndex;g=j.config.cache[l].normalized[h].length-1;j.config.cache[l].row[j.config.cache[l].normalized[h][g]]=m;j.config.cache[l].normalized[h][i]=e.parsers[i].format(getElementText(j,b,i),j,b,i);checkResort(d,c,f)}}).bind("addRows",function(a,b,c,f){var h,i=b.filter("tr").length,j=[],k=b[0].cells.length,l=this,m=$(this).find("tbody").index(b.closest("tbody"));for(h=0;h0){d.trigger("sorton",[e.sortList,{},!e.initWidgets])}else if(e.initWidgets){ts.applyWidget(this)}fixColumnWidth(this);if(e.showProcessing){d.unbind("sortBegin sortEnd").bind("sortBegin sortEnd",function(a){ts.isProcessing(d[0],a.type==="sortBegin")})}this.hasInitialized=true;if(e.debug){ts.benchmark("Overall initialization time",$.data(this,"startoveralltimer"))}d.trigger("tablesorter-initialized",this);if(typeof e.initialized==="function"){e.initialized(this)}})};ts.isProcessing=function(a,b,c){var d=a.config,e=c||$(a).find("."+d.cssHeader);if(b){if(d.sortList.length>0){e=e.filter(function(){return this.sortDisabled?false:ts.isValueInArray(parseFloat($(this).attr("data-column")),d.sortList)})}e.addClass(d.cssProcessing)}else{e.removeClass(d.cssProcessing)}};ts.processTbody=function(a,b,c){var d,e;if(c){b.before('');e=$.fn.detach?b.detach():b.remove();return e}e=$(a).find("span.tablesorter-savemyplace");b.insertAfter(e);e.remove()};ts.clearTableBody=function(a){$(a.tBodies).filter(":not(."+a.config.cssInfoBlock+")").empty()};ts.destroy=function(a,b,c){var d=$(a),e=a.config,f=d.find("thead:first");a.hasInitialized=false;f.find("tr:not(."+e.cssHeaderRow+")").remove();f.find(".tablesorter-resizer").remove();ts.refreshWidgets(a,true,true);d.removeData("tablesorter").unbind("update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave").find("."+e.cssHeader).unbind("click mousedown mousemove mouseup").removeClass(e.cssHeader+" "+e.cssAsc+" "+e.cssDesc).find(".tablesorter-header-inner").each(function(){if(e.cssIcon!==""){$(this).find("."+e.cssIcon).remove()}$(this).replaceWith($(this).contents())});if(b!==false){d.removeClass(e.tableClass)}if(typeof c==="function"){c(a)}};ts.regex=[/(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi,/(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,/^0x[0-9a-f]+$/i];ts.sortText=function(a,b,c,d){if(b===c){return 0}var e=a.config,f=e.string[e.empties[d]||e.emptyTo],g=ts.regex,h,i,j,k,l,m,n,o;if(b===""&&f!==0){return typeof f==="boolean"?f?-1:1:-f||-1}if(c===""&&f!==0){return typeof f==="boolean"?f?1:-1:f||1}if(typeof e.textSorter==="function"){return e.textSorter(b,c,a,d)}h=b.replace(g[0],"\\0$1\\0").replace(/\\0$/,"").replace(/^\\0/,"").split("\\0");j=c.replace(g[0],"\\0$1\\0").replace(/\\0$/,"").replace(/^\\0/,"").split("\\0");i=parseInt(b.match(g[2]),16)||h.length!==1&&b.match(g[1])&&Date.parse(b);k=parseInt(c.match(g[2]),16)||i&&c.match(g[1])&&Date.parse(c)||null;if(k){if(ik){return 1}}o=Math.max(h.length,j.length);for(n=0;nm){return 1}}return 0};ts.sortTextDesc=function(a,b,c,d){if(b===c){return 0}var e=a.config,f=e.string[e.empties[d]||e.emptyTo];if(b===""&&f!==0){return typeof f==="boolean"?f?-1:1:f||1}if(c===""&&f!==0){return typeof f==="boolean"?f?1:-1:-f||-1}if(typeof e.textSorter==="function"){return e.textSorter(c,b,a,d)}return ts.sortText(a,c,b)};ts.getTextValue=function(a,b,c){if(b){var d,e=a.length,f=b+c;for(d=0;d=0){c.widgets.splice(g,1);c.widgets.push("zebra")}if(c.debug){f=new Date}for(g=0;g1){g=0;e=d.children("tr:visible");e.each(function(){f=$(this);if(!l.test(this.className)){g++}h=g%2===0;f.removeClass(c.zebra[h?1:0]).addClass(c.zebra[h?0:1])})}}if(b.debug){ts.benchmark("Applying Zebra widget",i)}},remove:function(a,b,c){var d,e,f=$(a).children("tbody:not(."+b.cssInfoBlock+")"),g=(b.widgetOptions.zebra||["even","odd"]).join(" ");for(d=0;d Date: Sun, 21 Oct 2012 08:49:25 -0500 Subject: Fix navbar logo styling The relative path to the logo PNG image wasn't correct, and too much of the other styles got deleted when removing the IE6 compatibility shim. Signed-off-by: Dan McGee --- sitestatic/archweb.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 5c464ce3..e6c1a3b8 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -14,7 +14,7 @@ /* container for the entire bar */ #archnavbar { height: 40px !important; padding: 10px 15px !important; background: #333 !important; border-bottom: 5px #08c solid !important; } -#archnavbarlogo { background: url('archlogo.png') no-repeat !important; } +#archnavbarlogo { float: left !important; margin: 0 !important; padding: 0 !important; height: 40px !important; width: 190px !important; background: url('archnavbar/archlogo.png') no-repeat !important; } /* move the heading/paragraph text offscreen */ #archnavbarlogo p { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; } -- cgit v1.2.3-54-g00ecf From e398a957bb1cc338fe16548f0c6a330bea823c95 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 21 Oct 2012 10:15:19 -0500 Subject: Remove charset declaration tag This belongs in HTTP headers, not here. Signed-off-by: Dan McGee --- templates/base.html | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/base.html b/templates/base.html index 392bf22f..2617bc31 100644 --- a/templates/base.html +++ b/templates/base.html @@ -2,7 +2,6 @@ {% block title %}Arch Linux{% endblock %} - -- cgit v1.2.3-54-g00ecf From b3e3a05b98d18a09a03a0c82d878315f49f091af Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 21 Oct 2012 10:26:45 -0500 Subject: css: with ID selectors, no need for tag selector This simplifies a lot of the CSS and brings it in line with recommendations to use ID-based selectors when possible. Signed-off-by: Dan McGee --- sitestatic/archweb.css | 62 +++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index e6c1a3b8..7b2205e4 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -180,22 +180,22 @@ h5 { } /* general layout */ -div#content { +#content { width: 95%; margin: 0 auto; text-align: left; } -div#content-left-wrapper { +#content-left-wrapper { float: left; width: 100%; /* req to keep content above sidebar in source code */ } -div#content-left { +#content-left { margin: 0 340px 0 0; } -div#content-right { +#content-right { float: left; width: 300px; margin-left: -300px; @@ -208,12 +208,12 @@ div.box { border: 1px solid #bcd; } -div#footer { +#footer { clear: both; margin: 2em 0 1em; } - div#footer p { + #footer p { margin: 0; text-align: center; font-size: 0.85em; @@ -369,24 +369,24 @@ form.general-form textarea { } /* archdev navbar */ -div#archdev-navbar { +#archdev-navbar { margin: 1.5em 0; } - div#archdev-navbar ul { + #archdev-navbar ul { list-style: none; margin: -0.5em 0; padding: 0; } - div#archdev-navbar li { + #archdev-navbar li { display: inline; margin: 0; padding: 0; font-size: 0.9em; } - div#archdev-navbar li a { + #archdev-navbar li a { padding: 0 0.5em; color: #07b; } @@ -568,14 +568,14 @@ h3 span.arrow { } /* home: sidebar navigation */ -div#nav-sidebar ul { +#nav-sidebar ul { list-style: none; margin: 0.5em 0 0.5em 1em; padding: 0; } /* home: sponsor banners */ -div#arch-sponsors img { +#arch-sponsors img { padding: 0.3em 0; } @@ -619,21 +619,21 @@ div.news-article .article-info { } /* news: add/edit article */ -form#newsform { +#newsform { width: 60em; } - form#newsform input[type=text], - form#newsform textarea { + #newsform input[type=text], + #newsform textarea { width: 75%; } /* donate: donor list */ -div#donor-list ul { +#donor-list ul { width: 100%; } /* max 4 columns, but possibly fewer if screen size doesn't allow for more */ - div#donor-list li { + #donor-list li { float: left; width: 25%; min-width: 20em; @@ -698,7 +698,7 @@ table.results { } /* pkglist: layout */ -div#pkglist-about { +#pkglist-about { margin-top: 1.5em; } @@ -791,12 +791,12 @@ div#pkglist-about { } /* pkgdetails: flag package */ -form#flag-pkg-form label { +#flag-pkg-form label { width: 10em; } -form#flag-pkg-form textarea, -form#flag-pkg-form input[type=text] { +#flag-pkg-form textarea, +#flag-pkg-form input[type=text] { width: 45%; } @@ -872,24 +872,24 @@ table td.country { white-space: normal; } -form#list-generator div ul { +#list-generator div ul { list-style: none; display: inline; padding-left: 0; } - form#list-generator div ul li { + #list-generator div ul li { display: inline; } /* dev/TU biographies */ -div#arch-bio-toc { +#arch-bio-toc { width: 75%; margin: 0 auto; text-align: center; } - div#arch-bio-toc a { + #arch-bio-toc a { white-space: nowrap; } @@ -934,12 +934,12 @@ table.arch-bio-entry { } /* dev: login/out */ -table#dev-login { +#dev-login { width: auto; } /* dev dashboard: flagged packages */ -form#dash-pkg-notify { +#dash-pkg-notify { text-align: right; padding: 1em 0 0; margin-top: 1em; @@ -947,21 +947,21 @@ form#dash-pkg-notify { border-top: 1px dotted #bbb; } - form#dash-pkg-notify label { + #dash-pkg-notify label { width: auto; font-weight: normal; } - form#dash-pkg-notify input { + #dash-pkg-notify input { vertical-align: middle; margin: 0 0.25em; } - form#dash-pkg-notify input[type=submit] { + #dash-pkg-notify input[type=submit] { margin-top: -0.25em; } - form#dash-pkg-notify p { + #dash-pkg-notify p { margin: 0; } -- cgit v1.2.3-54-g00ecf From d92023710e98fda9c811e265fae682160ae01211 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 21 Oct 2012 10:27:39 -0500 Subject: Remove dead CSS Signed-off-by: Dan McGee --- sitestatic/archweb.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 7b2205e4..1d60f71d 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -644,10 +644,6 @@ div.news-article .article-info { border-bottom: 1px dotted #bbb; } -table#download-torrents .cpu-arch { - text-align: center; -} - /* pkglists/devlists */ table.results { font-size: 0.846em; -- cgit v1.2.3-54-g00ecf From ed9600e2209eb2c9fd755cc00878993fec998ef0 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 21 Oct 2012 10:27:54 -0500 Subject: More navbar styling cleanup Use ID-only rules, etc. Signed-off-by: Dan McGee --- sitestatic/archweb.css | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 1d60f71d..1af070ad 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -16,20 +16,19 @@ #archnavbar { height: 40px !important; padding: 10px 15px !important; background: #333 !important; border-bottom: 5px #08c solid !important; } #archnavbarlogo { float: left !important; margin: 0 !important; padding: 0 !important; height: 40px !important; width: 190px !important; background: url('archnavbar/archlogo.png') no-repeat !important; } -/* move the heading/paragraph text offscreen */ -#archnavbarlogo p { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; } +/* move the heading text offscreen */ #archnavbarlogo h1 { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; } /* make the link the same size as the logo */ #archnavbarlogo a { display: block !important; height: 40px !important; width: 190px !important; } /* display the list inline, float it to the right and style it */ -#archnavbar ul { display: inline !important; float: right !important; list-style: none !important; margin: 0 !important; padding: 0 !important; } -#archnavbar ul li { float: left !important; font-size: 14px !important; font-family: sans-serif !important; line-height: 45px !important; padding-right: 15px !important; padding-left: 15px !important; } +#archnavbarlist { display: inline !important; float: right !important; list-style: none !important; margin: 0 !important; padding: 0 !important; } +#archnavbarlist li { float: left !important; font-size: 14px !important; font-family: sans-serif !important; line-height: 45px !important; padding-right: 15px !important; padding-left: 15px !important; } /* style the links */ -#archnavbar ul#archnavbarlist li a { color: #999; font-weight: bold !important; text-decoration: none !important; } -#archnavbar ul li a:hover { color: white !important; text-decoration: underline !important; } +#archnavbarlist li a { color: #999; font-weight: bold !important; text-decoration: none !important; } +#archnavbarlist li a:hover { color: white !important; text-decoration: underline !important; } /* END ARCH GLOBAL NAVBAR */ -- cgit v1.2.3-54-g00ecf From 96cabfbdd6564a8b1e7cc7b365a194cbb6736fae Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 21 Oct 2012 10:30:33 -0500 Subject: Remove 'table' tag specifier from '.arch-bio-entry' Class-based selectors make sense here, we don't need the table tag too. Signed-off-by: Dan McGee --- sitestatic/archweb.css | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 1af070ad..2a91c393 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -888,33 +888,33 @@ table td.country { white-space: nowrap; } -table.arch-bio-entry { +.arch-bio-entry { width: 75%; min-width: 640px; margin: 0 auto; } - table.arch-bio-entry td.pic { + .arch-bio-entry td.pic { vertical-align: top; padding-right: 15px; padding-top: 2.25em; } - table.arch-bio-entry td.pic img { + .arch-bio-entry td.pic img { padding: 4px; border: 1px solid #ccc; } - table.arch-bio-entry td h3 { + .arch-bio-entry td h3 { border-bottom: 1px dotted #ccc; margin-bottom: 0.5em; } - table.arch-bio-entry table.bio { + .arch-bio-entry table.bio { margin-bottom: 2em; } - table.arch-bio-entry table.bio th { + .arch-bio-entry table.bio th { color: #666; font-weight: normal; text-align: right; @@ -923,7 +923,7 @@ table.arch-bio-entry { white-space: nowrap; } - table.arch-bio-entry table.bio td { + .arch-bio-entry table.bio td { width: 100%; padding-bottom: 0.25em; } -- cgit v1.2.3-54-g00ecf From 520066075938d325f93f814f92bb6005d00833c8 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 26 Oct 2012 16:47:46 -0500 Subject: Add .DS_Store to .gitignore Thanks for your silly files, OS X. Signed-off-by: Dan McGee --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a089ddd8..7ecfa380 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.pyc *.swp *.swo +.DS_Store local_settings.py archweb.db archweb.db-* -- cgit v1.2.3-54-g00ecf From 0b97d52351fc2bdcae16f1a1e7c56afd4ed476ad Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 26 Oct 2012 16:49:58 -0500 Subject: Enable safe mode for markdown parsing Although we don't allow unauthenticated users to post content, we should still cover our bases here and ensure people can't inject stuff into the production website via an inadvertent XSS. Signed-off-by: Dan McGee --- news/views.py | 2 +- templates/feeds/news_description.html | 2 +- templates/news/view.html | 2 +- templates/public/index.html | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/news/views.py b/news/views.py index 03f3b0ac..c0230f1e 100644 --- a/news/views.py +++ b/news/views.py @@ -76,7 +76,7 @@ def view_redirect(request, object_id): @require_POST def preview(request): data = request.POST.get('data', '') - markup = markdown.markdown(data) + markup = markdown.markdown(data, safe_mode=True) return HttpResponse(markup) # vim: set ts=4 sw=4 et: diff --git a/templates/feeds/news_description.html b/templates/feeds/news_description.html index e75d0af7..77830367 100644 --- a/templates/feeds/news_description.html +++ b/templates/feeds/news_description.html @@ -1,3 +1,3 @@ {% load markup %}

    {{obj.author.get_full_name}} wrote:

    -{{ obj.content|markdown }} \ No newline at end of file +{{ obj.content|markdown:'safe' }} diff --git a/templates/news/view.html b/templates/news/view.html index 445f0398..b6c06b28 100644 --- a/templates/news/view.html +++ b/templates/news/view.html @@ -28,6 +28,6 @@

    {{ news.title }}

    -
    {{ news.content|markdown }}
    +
    {{ news.content|markdown:'safe' }}
    {% endblock %} diff --git a/templates/public/index.html b/templates/public/index.html index 000a527b..762433a4 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -53,8 +53,8 @@

    {{ news.postdate|date }}

    - {% if forloop.counter0 == 0 %}{{ news.content|markdown|truncatewords_html:300 }} - {% else %}{{ news.content|markdown|truncatewords_html:100 }}{% endif %} + {% if forloop.counter0 == 0 %}{{ news.content|markdown:'safe'|truncatewords_html:300 }} + {% else %}{{ news.content|markdown:'safe'|truncatewords_html:100 }}{% endif %}
    {% else %} {% if forloop.counter0 == 5 %} -- cgit v1.2.3-54-g00ecf From 2ee662c77cf559d6ea82c9096533abfcd38f4801 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 26 Oct 2012 17:05:39 -0500 Subject: Extract some common architecture grabbing logic Signed-off-by: Dan McGee --- packages/views/display.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/views/display.py b/packages/views/display.py index b5cd643a..efedf6ff 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -13,11 +13,16 @@ from ..utils import get_group_info, PackageJSONEncoder +def arch_plus_agnostic(arch): + arches = [ arch ] + arches.extend(Arch.objects.filter(agnostic=True).order_by()) + return arches + + def split_package_details(request, name, repo, arch): '''Check if we have a split package (e.g. pkgbase) value matching this name. If so, we can show a listing page for the entire set of packages.''' - arches = [ arch ] - arches.extend(Arch.objects.filter(agnostic=True)) + arches = arch_plus_agnostic(arch) pkgs = Package.objects.normal().filter(pkgbase=name, repo__testing=repo.testing, repo__staging=repo.staging, arch__in=arches).order_by('pkgname') @@ -42,8 +47,7 @@ def recently_removed_package(request, name, repo, arch, cutoff=CUTOFF): '''Check our packages update table to see if this package has existed in this repo before. If so, we can show a 410 Gone page and point the requester in the right direction.''' - arches = [ arch ] - arches.extend(Arch.objects.filter(agnostic=True)) + arches = arch_plus_agnostic(arch) match = Update.objects.select_related('arch', 'repo').filter( pkgname=name, repo=repo, arch__in=arches) if cutoff is not None: @@ -149,8 +153,7 @@ def groups(request, arch=None): def group_details(request, arch, name): arch = get_object_or_404(Arch, name=arch) - arches = [ arch ] - arches.extend(Arch.objects.filter(agnostic=True)) + arches = arch_plus_agnostic(arch) pkgs = Package.objects.normal().filter( groups__name=name, arch__in=arches).order_by('pkgname') if len(pkgs) == 0: -- cgit v1.2.3-54-g00ecf From bdee24b9d1279de67dd238e3644c2efff314bd7b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 26 Oct 2012 17:11:11 -0500 Subject: Cleanup meta model attributes Signed-off-by: Dan McGee --- main/models.py | 5 ++--- news/models.py | 2 +- packages/models.py | 7 +++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/main/models.py b/main/models.py index 00549268..5700cdf1 100644 --- a/main/models.py +++ b/main/models.py @@ -62,7 +62,7 @@ def __lt__(self, other): class Meta: db_table = 'arches' - ordering = ['name'] + ordering = ('name',) verbose_name_plural = 'arches' @@ -87,8 +87,7 @@ def __lt__(self, other): class Meta: db_table = 'repos' - ordering = ['name'] - verbose_name_plural = 'repos' + ordering = ('name',) class Package(models.Model): diff --git a/news/models.py b/news/models.py index 2efea579..91232706 100644 --- a/news/models.py +++ b/news/models.py @@ -24,7 +24,7 @@ class Meta: db_table = 'news' verbose_name_plural = 'news' get_latest_by = 'postdate' - ordering = ['-postdate'] + ordering = ('-postdate',) def set_news_fields(sender, **kwargs): news = kwargs['instance'] diff --git a/packages/models.py b/packages/models.py index 0bea21b1..0d0fbdf2 100644 --- a/packages/models.py +++ b/packages/models.py @@ -329,6 +329,9 @@ class PackageGroup(models.Model): def __unicode__(self): return "%s: %s" % (self.name, self.pkg) + class Meta: + ordering = ('name',) + class License(models.Model): pkg = models.ForeignKey(Package, related_name='licenses') @@ -338,7 +341,7 @@ def __unicode__(self): return self.name class Meta: - ordering = ['name'] + ordering = ('name',) class RelatedToBase(models.Model): @@ -435,7 +438,7 @@ def __unicode__(self): class Meta: abstract = True - ordering = ['name'] + ordering = ('name',) class Depend(RelatedToBase): -- cgit v1.2.3-54-g00ecf From 62bb3db8ada68a22c7a58f32b2e6bed63f19e53c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 26 Oct 2012 17:36:12 -0500 Subject: Remove usages of 'django.contrib.markup' Switch to the news model being able to spit out the HTML version of the content, and don't use the markup contrib module. This is deprecated as of Django 1.5 so we can move off it now to save trouble down the road when it is fully removed. Signed-off-by: Dan McGee --- news/models.py | 7 +++++++ news/views.py | 2 +- settings.py | 1 - templates/feeds/news_description.html | 3 +-- templates/news/view.html | 3 +-- templates/public/index.html | 6 +++--- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/news/models.py b/news/models.py index 91232706..40238cde 100644 --- a/news/models.py +++ b/news/models.py @@ -1,6 +1,9 @@ +import markdown + from django.db import models from django.contrib.auth.models import User from django.contrib.sites.models import Site +from django.utils.safestring import mark_safe from django.utils.timezone import now @@ -17,6 +20,10 @@ class News(models.Model): def get_absolute_url(self): return '/news/%s/' % self.slug + def html(self): + return mark_safe(markdown.markdown( + self.content, safe_mode=True, enable_attributes=False)) + def __unicode__(self): return self.title diff --git a/news/views.py b/news/views.py index c0230f1e..74bec058 100644 --- a/news/views.py +++ b/news/views.py @@ -76,7 +76,7 @@ def view_redirect(request, object_id): @require_POST def preview(request): data = request.POST.get('data', '') - markup = markdown.markdown(data, safe_mode=True) + markup = markdown.markdown(data, safe_mode=True, enable_attributes=False) return HttpResponse(markup) # vim: set ts=4 sw=4 et: diff --git a/settings.py b/settings.py index 80df6f43..7038a71b 100644 --- a/settings.py +++ b/settings.py @@ -107,7 +107,6 @@ 'django.contrib.sites', 'django.contrib.sitemaps', 'django.contrib.admin', - 'django.contrib.markup', 'django.contrib.staticfiles', 'south', 'django_countries', diff --git a/templates/feeds/news_description.html b/templates/feeds/news_description.html index 77830367..d3cacebc 100644 --- a/templates/feeds/news_description.html +++ b/templates/feeds/news_description.html @@ -1,3 +1,2 @@ -{% load markup %}

    {{obj.author.get_full_name}} wrote:

    -{{ obj.content|markdown:'safe' }} +{{ obj.content.html }} diff --git a/templates/news/view.html b/templates/news/view.html index b6c06b28..8f49fb1f 100644 --- a/templates/news/view.html +++ b/templates/news/view.html @@ -1,5 +1,4 @@ {% extends "base.html" %} -{% load markup %} {% block title %}Arch Linux - News: {{ news.title }}{% endblock %} {% block content %} @@ -28,6 +27,6 @@

    {{ news.title }}

    -
    {{ news.content|markdown:'safe' }}
    +
    {{ news.html }}
    {% endblock %} diff --git a/templates/public/index.html b/templates/public/index.html index 762433a4..686fbdda 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load markup cache %} +{% load cache %} {% load url from future %} {% load static from staticfiles %} @@ -53,8 +53,8 @@

    {{ news.postdate|date }}

    - {% if forloop.counter0 == 0 %}{{ news.content|markdown:'safe'|truncatewords_html:300 }} - {% else %}{{ news.content|markdown:'safe'|truncatewords_html:100 }}{% endif %} + {% if forloop.counter0 == 0 %}{{ news.html|truncatewords_html:300 }} + {% else %}{{ news.html|truncatewords_html:100 }}{% endif %}
    {% else %} {% if forloop.counter0 == 5 %} -- cgit v1.2.3-54-g00ecf From ebc1d6098886b9d56d4f5ef6516cefafa7977c31 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 24 Oct 2012 22:05:58 -0500 Subject: Convert silhouette.png to an 8-bit colormap This cuts the size from 33KB to just 2KB. Signed-off-by: Dan McGee --- sitestatic/silhouette.png | Bin 33090 -> 2194 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/sitestatic/silhouette.png b/sitestatic/silhouette.png index afa87cd1..37e6cf33 100644 Binary files a/sitestatic/silhouette.png and b/sitestatic/silhouette.png differ -- cgit v1.2.3-54-g00ecf From bcccd16606a89507e5d5083440a50c98c576d380 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 27 Oct 2012 13:53:24 -0500 Subject: Fix news feed content display Signed-off-by: Dan McGee --- templates/feeds/news_description.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/feeds/news_description.html b/templates/feeds/news_description.html index d3cacebc..61ceedf3 100644 --- a/templates/feeds/news_description.html +++ b/templates/feeds/news_description.html @@ -1,2 +1,2 @@

    {{obj.author.get_full_name}} wrote:

    -{{ obj.content.html }} +{{ obj.html }} -- cgit v1.2.3-54-g00ecf From e2f7f3c9ea90d66c5d9678bd4b182917b03a2a90 Mon Sep 17 00:00:00 2001 From: Thomas Bächler Date: Mon, 29 Oct 2012 22:49:24 +0100 Subject: Remove information regarding test ISOs The test builds are dead and nobody is taking care of them. With our monthly releases, they serve no purpose. Signed-off-by: Dan McGee --- templates/public/download.html | 13 ------------- templates/public/index.html | 2 -- 2 files changed, 15 deletions(-) diff --git a/templates/public/download.html b/templates/public/download.html index 4b94e183..ba4c3282 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -57,19 +57,6 @@

    Netboot

    title="Arch Linux Netboot Live System">Arch Linux Netboot -

    Test ISO Info

    - -

    We provide daily snapshot ISOs. Those are largely untested, - but may be more up to date than the releases.

    - -

    HTTP Direct Downloads

    In addition to the BitTorrent links above, install images can also be diff --git a/templates/public/index.html b/templates/public/index.html index 686fbdda..2da03f4b 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -173,8 +173,6 @@

    Development

    title="View the available package groups">Package Groups
  • Todo Lists
  • -
  • Releng Testbuild Feedback
  • Visualizations
  • -- cgit v1.2.3-54-g00ecf From ce5b0b2c5c10a3c840fd8aaa696ec2b8f403dc5b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 31 Oct 2012 00:21:45 -0500 Subject: Disable markdown safe mode Unless we want older news items to look like [HTML_REMOVED]this[HTML_REMOVED] all over the place. I'm tempted to mark old items as non-safe but enforce safe mode for all new news postings. Signed-off-by: Dan McGee --- news/models.py | 2 +- news/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/news/models.py b/news/models.py index 40238cde..55d36318 100644 --- a/news/models.py +++ b/news/models.py @@ -22,7 +22,7 @@ def get_absolute_url(self): def html(self): return mark_safe(markdown.markdown( - self.content, safe_mode=True, enable_attributes=False)) + self.content, safe_mode=False, enable_attributes=False)) def __unicode__(self): return self.title diff --git a/news/views.py b/news/views.py index 74bec058..52182800 100644 --- a/news/views.py +++ b/news/views.py @@ -76,7 +76,7 @@ def view_redirect(request, object_id): @require_POST def preview(request): data = request.POST.get('data', '') - markup = markdown.markdown(data, safe_mode=True, enable_attributes=False) + markup = markdown.markdown(data, safe_mode=False, enable_attributes=False) return HttpResponse(markup) # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 4122e97f7aa5ebb919c2afc88a3e548a2ab1e2aa Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 31 Oct 2012 00:56:52 -0500 Subject: Store 'safe_mode' attribute on news model This lets us identify old news items that need to allow HTML through the markdown parser. For all new news items, we will disallow raw HTML. Signed-off-by: Dan McGee --- .../0011_auto__add_field_news_safe_mode.py | 68 ++++++++++++++++++++ news/migrations/0012_mark_old_news_safe_exempt.py | 73 ++++++++++++++++++++++ news/models.py | 3 +- 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 news/migrations/0011_auto__add_field_news_safe_mode.py create mode 100644 news/migrations/0012_mark_old_news_safe_exempt.py diff --git a/news/migrations/0011_auto__add_field_news_safe_mode.py b/news/migrations/0011_auto__add_field_news_safe_mode.py new file mode 100644 index 00000000..565c7adb --- /dev/null +++ b/news/migrations/0011_auto__add_field_news_safe_mode.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +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', 'safe_mode', + self.gf('django.db.models.fields.BooleanField')(default=True), + keep_default=True) + + def backwards(self, orm): + db.delete_column('news', 'safe_mode') + + 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'", 'on_delete': 'models.PROTECT', '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'}), + 'safe_mode': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['news'] diff --git a/news/migrations/0012_mark_old_news_safe_exempt.py b/news/migrations/0012_mark_old_news_safe_exempt.py new file mode 100644 index 00000000..b2661cd8 --- /dev/null +++ b/news/migrations/0012_mark_old_news_safe_exempt.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +import markdown + +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + md = markdown.Markdown(safe_mode=True, enable_attributes=False) + magic = md.html_replacement_text + items = orm.News.objects.all() + has_html = [item.pk for item in items if magic in md.convert(item.content)] + for pk in has_html: + orm.News.objects.filter(pk=pk).update(safe_mode=False) + + def backwards(self, orm): + orm.News.objects.all().update(safe_mode=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'", 'on_delete': 'models.PROTECT', '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'}), + 'safe_mode': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['news'] + symmetrical = True diff --git a/news/models.py b/news/models.py index 55d36318..42adc199 100644 --- a/news/models.py +++ b/news/models.py @@ -16,13 +16,14 @@ class News(models.Model): title = models.CharField(max_length=255) guid = models.CharField(max_length=255, editable=False) content = models.TextField() + safe_mode = models.BooleanField(default=True, editable=False) def get_absolute_url(self): return '/news/%s/' % self.slug def html(self): return mark_safe(markdown.markdown( - self.content, safe_mode=False, enable_attributes=False)) + self.content, safe_mode=self.safe_mode, enable_attributes=False)) def __unicode__(self): return self.title -- cgit v1.2.3-54-g00ecf From 1b28881f7ec58f87bc75699b2d906d28837bccb0 Mon Sep 17 00:00:00 2001 From: Thomas Bächler Date: Wed, 31 Oct 2012 15:09:51 +0100 Subject: download.html: Provide a magnet link Some people prefer these over torrent files, as they're easier to pass than the torrent files themselves. On updates, this only needs the new info hash. The tracker bits are optional, but ensure that the torrent client gets peers more quickly to receive the actual torrent file. Signed-off-by: Dan McGee --- templates/public/download.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/public/download.html b/templates/public/download.html index ba4c3282..14e77b6d 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -7,7 +7,7 @@ {% block navbarclass %}anb-download{% endblock %} {% block content %} -{% with version="2012.10.06" kernel_version="3.5.5" %} +{% with version="2012.10.06" kernel_version="3.5.5" torrent_infohash="b4865374f39a2b34f4c9517ec4b532f65094a28d" %}

    Arch Linux Downloads

    @@ -46,7 +46,8 @@

    BitTorrent Download (recommended)

    A web-seed capable client is recommended for fastest download speeds.

    Netboot

    -- cgit v1.2.3-54-g00ecf From 49546cddcab13c1767e386d6061493a25388b4bd Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 1 Nov 2012 08:29:30 -0500 Subject: Update for November ISO release Signed-off-by: Dan McGee --- templates/public/download.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/public/download.html b/templates/public/download.html index 14e77b6d..2fddd4e9 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -7,7 +7,7 @@ {% block navbarclass %}anb-download{% endblock %} {% block content %} -{% with version="2012.10.06" kernel_version="3.5.5" torrent_infohash="b4865374f39a2b34f4c9517ec4b532f65094a28d" %} +{% with version="2012.11.01" kernel_version="3.6.4" torrent_infohash="f86f84c74edc90336f94f0837afa3071ada2aaa8" %}

    Arch Linux Downloads

    -- cgit v1.2.3-54-g00ecf From df208fb0bd3f8e5783a6e41f411e7aa80179b2b6 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 1 Nov 2012 09:20:38 -0500 Subject: Signal attachment cleanup Signed-off-by: Dan McGee --- releng/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/releng/models.py b/releng/models.py index d602e9e5..bd178add 100644 --- a/releng/models.py +++ b/releng/models.py @@ -104,9 +104,9 @@ class Test(models.Model): success = models.BooleanField() comments = models.TextField(null=True, blank=True) -pre_save.connect(set_created_field, sender=Iso, - dispatch_uid="releng.models") -pre_save.connect(set_created_field, sender=Test, - dispatch_uid="releng.models") + +for model in (Iso, Test): + pre_save.connect(set_created_field, sender=model, + dispatch_uid="releng.models") # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 761084f280007e302fc9ae9c738b32fd0490bb70 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 1 Nov 2012 09:21:54 -0500 Subject: Allow editing news.safe_mode flag via admin screen We need to mark the property as editable, but you still don't have access to it through the normal non-admin views and edit screen. Signed-off-by: Dan McGee --- news/admin.py | 2 +- news/models.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/news/admin.py b/news/admin.py index 1b7de1d8..ad3cf517 100644 --- a/news/admin.py +++ b/news/admin.py @@ -3,7 +3,7 @@ from .models import News class NewsAdmin(admin.ModelAdmin): - list_display = ('title', 'author', 'postdate', 'last_modified') + list_display = ('title', 'author', 'postdate', 'last_modified', 'safe_mode') list_filter = ('postdate', 'author') search_fields = ('title', 'content') diff --git a/news/models.py b/news/models.py index 42adc199..d51db7c7 100644 --- a/news/models.py +++ b/news/models.py @@ -16,7 +16,7 @@ class News(models.Model): title = models.CharField(max_length=255) guid = models.CharField(max_length=255, editable=False) content = models.TextField() - safe_mode = models.BooleanField(default=True, editable=False) + safe_mode = models.BooleanField(default=True) def get_absolute_url(self): return '/news/%s/' % self.slug @@ -34,6 +34,7 @@ class Meta: get_latest_by = 'postdate' ordering = ('-postdate',) + def set_news_fields(sender, **kwargs): news = kwargs['instance'] current_time = now() -- cgit v1.2.3-54-g00ecf From 177b93ac21486c32afba543f77b9273f994f5e20 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 6 Nov 2012 08:18:53 -0600 Subject: Make guid feeds helper a @staticfunction Signed-off-by: Dan McGee --- feeds.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/feeds.py b/feeds.py index 55275ead..d00aec87 100644 --- a/feeds.py +++ b/feeds.py @@ -11,21 +11,24 @@ from main.models import Arch, Repo, Package from news.models import News -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): + @staticmethod + 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 + 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) + handler.addQuickElement = self.check_for_unique_id( + handler.addQuickElement) super(GuidNotPermalinkFeed, self).write_items(handler) -- cgit v1.2.3-54-g00ecf From 554df3f8cfdce7a41904ac985dc2e6f3d43c358d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 6 Nov 2012 08:22:44 -0600 Subject: Remove explicit timezone handling from feeds Signed-off-by: Dan McGee --- feeds.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/feeds.py b/feeds.py index d00aec87..49e0bc6e 100644 --- a/feeds.py +++ b/feeds.py @@ -1,5 +1,4 @@ import hashlib -import pytz from django.contrib.sites.models import Site from django.contrib.syndication.views import Feed @@ -106,7 +105,7 @@ def item_guid(self, item): date.strftime('%Y%m%d%H%M')) def item_pubdate(self, item): - return item.last_update.replace(tzinfo=pytz.utc) + return item.last_update def item_categories(self, item): return (item.repo.name, item.arch.name) @@ -144,7 +143,7 @@ def item_guid(self, item): return item.guid def item_pubdate(self, item): - return item.postdate.replace(tzinfo=pytz.utc) + return item.postdate def item_author_name(self, item): return item.author.get_full_name() -- cgit v1.2.3-54-g00ecf From 4ab5d6947795f1fef0d38601ec7ad3ca5f62173e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 10 Nov 2012 14:13:34 -0600 Subject: Add mirror extended status JSON view When asking for status for a single mirror, we can include logs from the past 24 hours in addition to the normal information we provide. This is slated for usage by a frontend graph still to come, similar to those on the NTP pool website. Signed-off-by: Dan McGee --- mirrors/urls.py | 1 + mirrors/views.py | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/mirrors/urls.py b/mirrors/urls.py index bb4eb969..857e99e2 100644 --- a/mirrors/urls.py +++ b/mirrors/urls.py @@ -6,6 +6,7 @@ (r'^status/json/$', 'status_json', {}, 'mirror-status-json'), (r'^status/tier/(?P\d+)/$', 'status', {}, 'mirror-status-tier'), (r'^(?P[\.\-\w]+)/$', 'mirror_details'), + (r'^(?P[\.\-\w]+)/json/$', 'mirror_details_json'), ) # vim: set ts=4 sw=4 et: diff --git a/mirrors/views.py b/mirrors/views.py index 11719223..cbd86611 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -9,10 +9,11 @@ from django.db.models import Q from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, render +from django.utils.timezone import now from django.views.decorators.csrf import csrf_exempt from django_countries.countries import COUNTRIES -from .models import Mirror, MirrorUrl, MirrorProtocol +from .models import Mirror, MirrorUrl, MirrorProtocol, MirrorLog from .utils import get_mirror_statuses, get_mirror_errors COUNTRY_LOOKUP = dict(COUNTRIES) @@ -183,6 +184,19 @@ def mirror_details(request, name): {'mirror': mirror, 'urls': all_urls}) +def mirror_details_json(request, name): + mirror = get_object_or_404(Mirror, name=name) + status_info = get_mirror_statuses() + data = status_info.copy() + data['version'] = 3 + # include only URLs for this particular mirror + data['urls'] = [url for url in data['urls'] if url.mirror_id == mirror.id] + to_json = json.dumps(data, ensure_ascii=False, + cls=ExtendedMirrorStatusJSONEncoder) + response = HttpResponse(to_json, mimetype='application/json') + return response + + def status(request, tier=None): if tier is not None: tier = int(tier) @@ -222,8 +236,8 @@ def status(request, tier=None): class MirrorStatusJSONEncoder(DjangoJSONEncoder): '''Base JSONEncoder extended to handle datetime.timedelta and MirrorUrl serialization. The base class takes care of datetime.datetime types.''' - url_attributes = ['url', 'protocol', 'last_sync', 'completion_pct', - 'delay', 'duration_avg', 'duration_stddev', 'score'] + url_attributes = ('url', 'protocol', 'last_sync', 'completion_pct', + 'delay', 'duration_avg', 'duration_stddev', 'score') def default(self, obj): if isinstance(obj, timedelta): @@ -245,6 +259,23 @@ def default(self, obj): return super(MirrorStatusJSONEncoder, self).default(obj) +class ExtendedMirrorStatusJSONEncoder(MirrorStatusJSONEncoder): + '''Adds URL check history information.''' + log_attributes = ('check_time', 'last_sync', 'duration', 'is_success') + + def default(self, obj): + if isinstance(obj, MirrorUrl): + data = super(ExtendedMirrorStatusJSONEncoder, self).default(obj) + cutoff = now() - timedelta(hours=24) + data['logs'] = obj.logs.filter(check_time__gte=cutoff) + return data + if isinstance(obj, MirrorLog): + data = dict((attr, getattr(obj, attr)) + for attr in self.log_attributes) + return data + return super(ExtendedMirrorStatusJSONEncoder, self).default(obj) + + def status_json(request): status_info = get_mirror_statuses() data = status_info.copy() -- cgit v1.2.3-54-g00ecf From 86102c6e645451c03e3e576060eba7f93350bf6b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 10 Nov 2012 14:19:23 -0600 Subject: Allow filtering retrieved mirror statuses by mirror_id When we don't need them all, no need to fetch them all. Let the database do the work for us, hopefully. Signed-off-by: Dan McGee --- mirrors/utils.py | 19 +++++++++++++++---- mirrors/views.py | 8 ++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/mirrors/utils.py b/mirrors/utils.py index 0a32b766..ba027c99 100644 --- a/mirrors/utils.py +++ b/mirrors/utils.py @@ -31,7 +31,7 @@ def annotate_url(url, delays): @cache_function(123) -def get_mirror_statuses(cutoff=DEFAULT_CUTOFF): +def get_mirror_statuses(cutoff=DEFAULT_CUTOFF, mirror_ids=None): cutoff_time = now() - cutoff # I swear, this actually has decent performance... urls = MirrorUrl.objects.select_related('mirror', 'protocol').filter( @@ -43,6 +43,9 @@ def get_mirror_statuses(cutoff=DEFAULT_CUTOFF): last_check=Max('logs__check_time'), duration_avg=Avg('logs__duration')) + if mirror_ids: + urls = urls.filter(mirror_id__in=mirror_ids) + vendor = database_vendor(MirrorUrl) if vendor != 'sqlite': urls = urls.annotate(duration_stddev=StdDev('logs__duration')) @@ -54,6 +57,8 @@ def get_mirror_statuses(cutoff=DEFAULT_CUTOFF): 'url_id', 'check_time', 'last_sync').filter( is_success=True, last_sync__isnull=False, check_time__gte=cutoff_time) + if mirror_ids: + times = times.filter(url__mirror_id__in=mirror_ids) delays = {} for url_id, check_time, last_sync in times: delay = check_time - last_sync @@ -62,8 +67,10 @@ def get_mirror_statuses(cutoff=DEFAULT_CUTOFF): if urls: last_check = max([u.last_check for u in urls]) num_checks = max([u.check_count for u in urls]) - check_info = MirrorLog.objects.filter( - check_time__gte=cutoff_time).aggregate( + check_info = MirrorLog.objects.filter(check_time__gte=cutoff_time) + if mirror_ids: + check_info = check_info.filter(url__mirror_id__in=mirror_ids) + check_info = check_info.aggregate( mn=Min('check_time'), mx=Max('check_time')) if num_checks > 1: check_frequency = (check_info['mx'] - check_info['mn']) \ @@ -91,7 +98,7 @@ def get_mirror_statuses(cutoff=DEFAULT_CUTOFF): @cache_function(117) -def get_mirror_errors(cutoff=DEFAULT_CUTOFF): +def get_mirror_errors(cutoff=DEFAULT_CUTOFF, mirror_ids=None): cutoff_time = now() - cutoff errors = MirrorLog.objects.filter( is_success=False, check_time__gte=cutoff_time, @@ -100,6 +107,10 @@ def get_mirror_errors(cutoff=DEFAULT_CUTOFF): 'url__mirror__country', 'url__mirror__tier', 'error').annotate( error_count=Count('error'), last_occurred=Max('check_time') ).order_by('-last_occurred', '-error_count') + + if mirror_ids: + urls = urls.filter(mirror_id__in=mirror_ids) + errors = list(errors) for err in errors: ctry_code = err['url__country'] or err['url__mirror__country'] diff --git a/mirrors/views.py b/mirrors/views.py index cbd86611..4b9721dc 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -14,7 +14,7 @@ from django_countries.countries import COUNTRIES from .models import Mirror, MirrorUrl, MirrorProtocol, MirrorLog -from .utils import get_mirror_statuses, get_mirror_errors +from .utils import get_mirror_statuses, get_mirror_errors, DEFAULT_CUTOFF COUNTRY_LOOKUP = dict(COUNTRIES) @@ -171,7 +171,7 @@ def mirror_details(request, name): (not mirror.public or not mirror.active): raise Http404 - status_info = get_mirror_statuses() + status_info = get_mirror_statuses(mirror_ids=[mirror.id]) checked_urls = [url for url in status_info['urls'] \ if url.mirror_id == mirror.id] all_urls = mirror.urls.select_related('protocol') @@ -186,7 +186,7 @@ def mirror_details(request, name): def mirror_details_json(request, name): mirror = get_object_or_404(Mirror, name=name) - status_info = get_mirror_statuses() + status_info = get_mirror_statuses(mirror_ids=[mirror.id]) data = status_info.copy() data['version'] = 3 # include only URLs for this particular mirror @@ -266,7 +266,7 @@ class ExtendedMirrorStatusJSONEncoder(MirrorStatusJSONEncoder): def default(self, obj): if isinstance(obj, MirrorUrl): data = super(ExtendedMirrorStatusJSONEncoder, self).default(obj) - cutoff = now() - timedelta(hours=24) + cutoff = now() - DEFAULT_CUTOFF data['logs'] = obj.logs.filter(check_time__gte=cutoff) return data if isinstance(obj, MirrorLog): -- cgit v1.2.3-54-g00ecf From 07d2fc5d358992a52908cccbbca4a11d01e98da3 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 10 Nov 2012 15:41:09 -0600 Subject: Add initial version of mirror status chart Still have some hardcoded stuff to rip out and replace to make this a bit more dynamic on things like sizing, but for now, this is a great start. Signed-off-by: Dan McGee --- mirrors/static/mirror_status.js | 97 +++++++++++++++++++++++++++++++++++ sitestatic/archweb.css | 11 ++++ templates/mirrors/mirror_details.html | 7 +++ 3 files changed, 115 insertions(+) create mode 100644 mirrors/static/mirror_status.js diff --git a/mirrors/static/mirror_status.js b/mirrors/static/mirror_status.js new file mode 100644 index 00000000..1c510cee --- /dev/null +++ b/mirrors/static/mirror_status.js @@ -0,0 +1,97 @@ +function mirror_status(chart_id, data_url) { + d3.json(data_url, function(json) { + data = jQuery.map(json['urls'], + function(url, i) { + return jQuery.map(url['logs'], + function(log, j) { + return { + url: url['url'], + duration: log['duration'], + check_time: new Date(log['check_time']) + }; + }); + }); + + var margin = {top: 20, right: 20, bottom: 30, left: 40}, + width = 1200 - margin.left - margin.right, + height = 450 - margin.top - margin.bottom; + + var color = d3.scale.category20(), + x = d3.time.scale.utc().range([0, width]), + y = d3.scale.linear().range([height, 0]), + x_axis = d3.svg.axis().scale(x).orient("bottom"), + y_axis = d3.svg.axis().scale(y).orient("left"); + + var svg = d3.select(chart_id).append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + x.domain(d3.extent(data, function(d) { return d.check_time; })).nice(d3.time.hour); + y.domain(d3.extent(data, function(d) { return d.duration; })).nice(); + + svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + height + ")") + .call(x_axis) + .append("text") + .attr("class", "label") + .attr("x", width) + .attr("y", -6) + .style("text-anchor", "end") + .text("Check Time (UTC)"); + + svg.append("g") + .attr("class", "y axis") + .call(y_axis) + .append("text") + .attr("class", "label") + .attr("transform", "rotate(-90)") + .attr("y", 6) + .attr("dy", ".71em") + .style("text-anchor", "end") + .text("Duration (seconds)"); + + svg.selectAll(".dot") + .data(data) + .enter() + .append("circle") + .attr("class", "dot") + .attr("r", 3.5) + .attr("cx", function(d) { return x(d.check_time); }) + .attr("cy", function(d) { return y(d.duration); }) + .style("fill", function(d) { return color(d.url); }); + + var legend = svg.selectAll(".legend") + .data(color.domain()) + .enter().append("g") + .attr("class", "legend") + .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; }); + + legend.append("rect") + .attr("x", width - 18) + .attr("width", 18) + .attr("height", 18) + .style("fill", color); + + legend.append("text") + .attr("x", width - 24) + .attr("y", 9) + .attr("dy", ".35em") + .style("text-anchor", "end") + .text(function(d) { return d; }); + }); + + var resize_timeout = null; + var real_resize = function() { + resize_timeout = null; + /* TODO: implement resize */ + }; + jQuery(window).resize(function() { + if (resize_timeout) { + clearTimeout(resize_timeout); + } + resize_timeout = setTimeout(real_resize, 200); + }); +} diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 2a91c393..24a0f121 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -877,6 +877,17 @@ table td.country { display: inline; } +#visualize-mirror .axis path, +#visualize-mirror .axis line { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} + +#visualize-mirror .dot { + stroke: #000; +} + /* dev/TU biographies */ #arch-bio-toc { width: 75%; diff --git a/templates/mirrors/mirror_details.html b/templates/mirrors/mirror_details.html index 18175845..32ef8a7a 100644 --- a/templates/mirrors/mirror_details.html +++ b/templates/mirrors/mirror_details.html @@ -104,14 +104,21 @@

    Available URLs

    {% endfor %}
    Alias:{{ prof.alias }}{{ prof.alias }}
    Email: {{ prof.public_email }}
    Website:{% if prof.website %}{% if prof.website %}{% endif %}
    {{ prof.occupation }}
    YOB:{% if prof.yob %}{{ prof.yob }}{% endif %}{% if prof.yob %}{{ prof.yob }}{% endif %}
    Location: {% if dev.userprofile.country %}{{ dev.userprofile.country.name }} {% endif %}{{ prof.location }}
    All ProjectsFeedFeedFeedFeedFeedFeed
    Arch Linux Feed
    + +

    Mirror Status Chart

    + +
    {% load cdn %}{% jquery %}{% jquery_tablesorter %} + + {% endblock %} -- cgit v1.2.3-54-g00ecf From aeeb4718e83cd2f82d94b1aa0c0ba36ba21a2b37 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 10 Nov 2012 16:12:15 -0600 Subject: Enable mirror status graph resizing Signed-off-by: Dan McGee --- mirrors/static/mirror_status.js | 43 +++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/mirrors/static/mirror_status.js b/mirrors/static/mirror_status.js index 1c510cee..277d3c88 100644 --- a/mirrors/static/mirror_status.js +++ b/mirrors/static/mirror_status.js @@ -1,20 +1,10 @@ function mirror_status(chart_id, data_url) { - d3.json(data_url, function(json) { - data = jQuery.map(json['urls'], - function(url, i) { - return jQuery.map(url['logs'], - function(log, j) { - return { - url: url['url'], - duration: log['duration'], - check_time: new Date(log['check_time']) - }; - }); - }); + var jq_div = jQuery(chart_id); + var draw_graph = function(data) { var margin = {top: 20, right: 20, bottom: 30, left: 40}, - width = 1200 - margin.left - margin.right, - height = 450 - margin.top - margin.bottom; + width = jq_div.width() - margin.left - margin.right, + height = jq_div.height() - margin.top - margin.bottom; var color = d3.scale.category20(), x = d3.time.scale.utc().range([0, width]), @@ -22,6 +12,8 @@ function mirror_status(chart_id, data_url) { x_axis = d3.svg.axis().scale(x).orient("bottom"), y_axis = d3.svg.axis().scale(y).orient("left"); + /* remove any existing graph first if we are redrawing after resize */ + d3.select(chart_id).select("svg").remove(); var svg = d3.select(chart_id).append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) @@ -31,6 +23,7 @@ function mirror_status(chart_id, data_url) { x.domain(d3.extent(data, function(d) { return d.check_time; })).nice(d3.time.hour); y.domain(d3.extent(data, function(d) { return d.duration; })).nice(); + /* build the axis lines... */ svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") @@ -53,6 +46,7 @@ function mirror_status(chart_id, data_url) { .style("text-anchor", "end") .text("Duration (seconds)"); + /* ...then the points themselves. */ svg.selectAll(".dot") .data(data) .enter() @@ -63,6 +57,7 @@ function mirror_status(chart_id, data_url) { .attr("cy", function(d) { return y(d.duration); }) .style("fill", function(d) { return color(d.url); }); + /* add a legend for good measure */ var legend = svg.selectAll(".legend") .data(color.domain()) .enter().append("g") @@ -81,12 +76,30 @@ function mirror_status(chart_id, data_url) { .attr("dy", ".35em") .style("text-anchor", "end") .text(function(d) { return d; }); + }; + + /* invoke the data-fetch + first draw */ + var cached_data = null; + d3.json(data_url, function(json) { + cached_data = jQuery.map(json['urls'], + function(url, i) { + return jQuery.map(url['logs'], + function(log, j) { + return { + url: url['url'], + duration: log['duration'], + check_time: new Date(log['check_time']) + }; + }); + }); + draw_graph(cached_data); }); + /* then hook up a resize handler to redraw if necessary */ var resize_timeout = null; var real_resize = function() { resize_timeout = null; - /* TODO: implement resize */ + draw_graph(cached_data); }; jQuery(window).resize(function() { if (resize_timeout) { -- cgit v1.2.3-54-g00ecf From a358e132886972dc4e9f1f546e36a5f3a2218a39 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 10 Nov 2012 17:09:51 -0600 Subject: Mirror status graph, now with points AND lines We might have to tweak the interpolation method once we see this with real data, but for now it looks really pretty locally. Signed-off-by: Dan McGee --- mirrors/static/mirror_status.js | 58 +++++++++++++++++++++++++++++------------ mirrors/views.py | 3 ++- sitestatic/archweb.css | 7 ++++- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/mirrors/static/mirror_status.js b/mirrors/static/mirror_status.js index 277d3c88..d107d7d1 100644 --- a/mirrors/static/mirror_status.js +++ b/mirrors/static/mirror_status.js @@ -6,7 +6,7 @@ function mirror_status(chart_id, data_url) { width = jq_div.width() - margin.left - margin.right, height = jq_div.height() - margin.top - margin.bottom; - var color = d3.scale.category20(), + var color = d3.scale.category10(), x = d3.time.scale.utc().range([0, width]), y = d3.scale.linear().range([height, 0]), x_axis = d3.svg.axis().scale(x).orient("bottom"), @@ -20,8 +20,14 @@ function mirror_status(chart_id, data_url) { .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); - x.domain(d3.extent(data, function(d) { return d.check_time; })).nice(d3.time.hour); - y.domain(d3.extent(data, function(d) { return d.duration; })).nice(); + x.domain([ + d3.min(data, function(c) { return d3.min(c.logs, function(v) { return v.check_time; }); }), + d3.max(data, function(c) { return d3.max(c.logs, function(v) { return v.check_time; }); }) + ]); + y.domain([ + d3.min(data, function(c) { return d3.min(c.logs, function(v) { return v.duration; }); }), + d3.max(data, function(c) { return d3.max(c.logs, function(v) { return v.duration; }); }) + ]); /* build the axis lines... */ svg.append("g") @@ -46,12 +52,32 @@ function mirror_status(chart_id, data_url) { .style("text-anchor", "end") .text("Duration (seconds)"); - /* ...then the points themselves. */ - svg.selectAll(".dot") + var line = d3.svg.line() + .interpolate("basis") + .x(function(d) { return x(d.check_time); }) + .y(function(d) { return y(d.duration); }); + + /* ...then the points and lines between them. */ + var urls = svg.selectAll(".url") .data(data) .enter() + .append("g") + .attr("class", "url"); + + urls.append("path") + .attr("class", "url-line") + .attr("d", function(d) { return line(d.logs); }) + .style("stroke", function(d) { return color(d.url); }); + + urls.selectAll("circle") + .data(function(u) { + return jQuery.map(u.logs, function(l, i) { + return {url: u.url, check_time: l.check_time, duration: l.duration}; + }); + }) + .enter() .append("circle") - .attr("class", "dot") + .attr("class", "url-dot") .attr("r", 3.5) .attr("cx", function(d) { return x(d.check_time); }) .attr("cy", function(d) { return y(d.duration); }) @@ -81,16 +107,16 @@ function mirror_status(chart_id, data_url) { /* invoke the data-fetch + first draw */ var cached_data = null; d3.json(data_url, function(json) { - cached_data = jQuery.map(json['urls'], - function(url, i) { - return jQuery.map(url['logs'], - function(log, j) { - return { - url: url['url'], - duration: log['duration'], - check_time: new Date(log['check_time']) - }; - }); + cached_data = jQuery.map(json.urls, function(url, i) { + return { + url: url.url, + logs: jQuery.map(url.logs, function(log, j) { + return { + duration: log.duration, + check_time: new Date(log.check_time) + }; + }) + }; }); draw_graph(cached_data); }); diff --git a/mirrors/views.py b/mirrors/views.py index 4b9721dc..be01e919 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -267,7 +267,8 @@ def default(self, obj): if isinstance(obj, MirrorUrl): data = super(ExtendedMirrorStatusJSONEncoder, self).default(obj) cutoff = now() - DEFAULT_CUTOFF - data['logs'] = obj.logs.filter(check_time__gte=cutoff) + data['logs'] = obj.logs.filter( + check_time__gte=cutoff).order_by('check_time') return data if isinstance(obj, MirrorLog): data = dict((attr, getattr(obj, attr)) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 24a0f121..7ef85016 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -884,10 +884,15 @@ table td.country { shape-rendering: crispEdges; } -#visualize-mirror .dot { +#visualize-mirror .url-dot { stroke: #000; } +#visualize-mirror .url-line { + fill: none; + stroke-width: 1.5px; +} + /* dev/TU biographies */ #arch-bio-toc { width: 75%; -- cgit v1.2.3-54-g00ecf From 2e8d3e99e3dc756508c4398a2cb1e6ce5d9dcb71 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 10 Nov 2012 17:12:41 -0600 Subject: Slight style tweaks Signed-off-by: Dan McGee --- sitestatic/archweb.css | 1 + 1 file changed, 1 insertion(+) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 7ef85016..cef41399 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -881,6 +881,7 @@ table td.country { #visualize-mirror .axis line { fill: none; stroke: #000; + stroke-width: 3px; shape-rendering: crispEdges; } -- cgit v1.2.3-54-g00ecf From 923ebbb53abf1d77a2f21b76e88faa085251af78 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 10 Nov 2012 17:20:26 -0600 Subject: Re-add nice() calls for mirror status axes Signed-off-by: Dan McGee --- mirrors/static/mirror_status.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mirrors/static/mirror_status.js b/mirrors/static/mirror_status.js index d107d7d1..1c352a9f 100644 --- a/mirrors/static/mirror_status.js +++ b/mirrors/static/mirror_status.js @@ -23,11 +23,11 @@ function mirror_status(chart_id, data_url) { x.domain([ d3.min(data, function(c) { return d3.min(c.logs, function(v) { return v.check_time; }); }), d3.max(data, function(c) { return d3.max(c.logs, function(v) { return v.check_time; }); }) - ]); + ]).nice(d3.time.hour); y.domain([ d3.min(data, function(c) { return d3.min(c.logs, function(v) { return v.duration; }); }), d3.max(data, function(c) { return d3.max(c.logs, function(v) { return v.duration; }); }) - ]); + ]).nice(); /* build the axis lines... */ svg.append("g") -- cgit v1.2.3-54-g00ecf From e26d5722289bd2e972633891d8dac09296b0cbc4 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 11 Nov 2012 14:55:37 -0600 Subject: Mirror graph tweaking after usage with real data * Clamp y-axis minimum to 0. * Don't plot `is_success == false` values. * Ensure URLs are sorted predictably. Signed-off-by: Dan McGee --- mirrors/static/mirror_status.js | 5 ++++- mirrors/utils.py | 3 ++- mirrors/views.py | 2 -- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mirrors/static/mirror_status.js b/mirrors/static/mirror_status.js index 1c352a9f..decc8fb8 100644 --- a/mirrors/static/mirror_status.js +++ b/mirrors/static/mirror_status.js @@ -25,7 +25,7 @@ function mirror_status(chart_id, data_url) { d3.max(data, function(c) { return d3.max(c.logs, function(v) { return v.check_time; }); }) ]).nice(d3.time.hour); y.domain([ - d3.min(data, function(c) { return d3.min(c.logs, function(v) { return v.duration; }); }), + 0, d3.max(data, function(c) { return d3.max(c.logs, function(v) { return v.duration; }); }) ]).nice(); @@ -111,6 +111,9 @@ function mirror_status(chart_id, data_url) { return { url: url.url, logs: jQuery.map(url.logs, function(log, j) { + if (!log.is_success) { + return null; + } return { duration: log.duration, check_time: new Date(log.check_time) diff --git a/mirrors/utils.py b/mirrors/utils.py index ba027c99..85e4ee93 100644 --- a/mirrors/utils.py +++ b/mirrors/utils.py @@ -41,7 +41,8 @@ def get_mirror_statuses(cutoff=DEFAULT_CUTOFF, mirror_ids=None): success_count=Count('logs__duration'), last_sync=Max('logs__last_sync'), last_check=Max('logs__check_time'), - duration_avg=Avg('logs__duration')) + duration_avg=Avg('logs__duration')).order_by( + 'mirror', 'url') if mirror_ids: urls = urls.filter(mirror_id__in=mirror_ids) diff --git a/mirrors/views.py b/mirrors/views.py index be01e919..5e374b4d 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -189,8 +189,6 @@ def mirror_details_json(request, name): status_info = get_mirror_statuses(mirror_ids=[mirror.id]) data = status_info.copy() data['version'] = 3 - # include only URLs for this particular mirror - data['urls'] = [url for url in data['urls'] if url.mirror_id == mirror.id] to_json = json.dumps(data, ensure_ascii=False, cls=ExtendedMirrorStatusJSONEncoder) response = HttpResponse(to_json, mimetype='application/json') -- cgit v1.2.3-54-g00ecf From d616a40cf57d417775d7277a6d03f51c2967e8d9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 11 Nov 2012 15:47:44 -0600 Subject: Exclude news.safe_mode on news edit screen Signed-off-by: Dan McGee --- news/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/views.py b/news/views.py index 52182800..efd93fdb 100644 --- a/news/views.py +++ b/news/views.py @@ -28,7 +28,7 @@ def find_unique_slug(newsitem): class NewsForm(forms.ModelForm): class Meta: model = News - exclude = ('id', 'slug', 'author', 'postdate') + exclude = ('id', 'slug', 'author', 'postdate', 'safe_mode') class NewsDetailView(DetailView): -- cgit v1.2.3-54-g00ecf From 99bfdda5f257107396179694b7c56aad8e5e6701 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 11 Nov 2012 16:40:21 -0600 Subject: Add title to mirror status data points Signed-off-by: Dan McGee --- mirrors/static/mirror_status.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mirrors/static/mirror_status.js b/mirrors/static/mirror_status.js index decc8fb8..8ec85c40 100644 --- a/mirrors/static/mirror_status.js +++ b/mirrors/static/mirror_status.js @@ -81,7 +81,9 @@ function mirror_status(chart_id, data_url) { .attr("r", 3.5) .attr("cx", function(d) { return x(d.check_time); }) .attr("cy", function(d) { return y(d.duration); }) - .style("fill", function(d) { return color(d.url); }); + .style("fill", function(d) { return color(d.url); }) + .append("title") + .text(function(d) { return d.url + "\n" + d.duration.toFixed(3) + " secs\n" + d.check_time.toUTCString(); }); /* add a legend for good measure */ var legend = svg.selectAll(".legend") -- cgit v1.2.3-54-g00ecf From 55d2d3ae51e1d0aa0927d682b45a3500588ed07b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 12 Nov 2012 09:41:28 -0600 Subject: Add get_latest_by to MirrorLog Meta class Signed-off-by: Dan McGee --- mirrors/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mirrors/models.py b/mirrors/models.py index 06b483d5..384668b8 100644 --- a/mirrors/models.py +++ b/mirrors/models.py @@ -131,5 +131,6 @@ def __unicode__(self): class Meta: verbose_name = 'mirror check log' + get_latest_by = 'check_time' # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 6859a7568c625cb448f4dd3f25a81caa3cbfcc04 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 12 Nov 2012 09:43:32 -0600 Subject: Add comments to visualize JS Signed-off-by: Dan McGee --- visualize/static/visualize.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/visualize/static/visualize.js b/visualize/static/visualize.js index 307c38c4..7e240d44 100644 --- a/visualize/static/visualize.js +++ b/visualize/static/visualize.js @@ -115,6 +115,7 @@ function packages_treemap(chart_id, orderings, default_order) { make_group_button(k, v); }); + /* adapt the chart size when the browser resizes */ var resize_timeout = null; var real_resize = function() { resize_timeout = null; @@ -275,6 +276,7 @@ function developer_keys(chart_id, data_url) { .start(); }); + /* adapt the chart size when the browser resizes */ var resize_timeout = null; var real_resize = function() { resize_timeout = null; -- cgit v1.2.3-54-g00ecf From a8a0f013f53ef85ed222fb501f113670f8c6340d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 12 Nov 2012 09:52:25 -0600 Subject: Add a test high-resolution image Just the simple RSS feed image for now, but want to see how complicated this will get. Signed-off-by: Dan McGee --- sitestatic/rss@2x.png | Bin 0 -> 1466 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 sitestatic/rss@2x.png diff --git a/sitestatic/rss@2x.png b/sitestatic/rss@2x.png new file mode 100644 index 00000000..ffd6feba Binary files /dev/null and b/sitestatic/rss@2x.png differ -- cgit v1.2.3-54-g00ecf From 2339f42ef0f95e55d99be47ed2327c3d127ebc29 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 29 Oct 2012 20:07:54 -0500 Subject: Remove SevenL from sponsors list They've been good to us for several years, but our hardware requirements have changed and we've moved on to using different machines for the purposes this donated machine served. Thanks! Signed-off-by: Dan McGee --- sitestatic/sevenl_button.png | Bin 6840 -> 0 bytes templates/public/donate.html | 14 -------------- templates/public/index.html | 5 ----- 3 files changed, 19 deletions(-) delete mode 100644 sitestatic/sevenl_button.png diff --git a/sitestatic/sevenl_button.png b/sitestatic/sevenl_button.png deleted file mode 100644 index 93adcdf0..00000000 Binary files a/sitestatic/sevenl_button.png and /dev/null differ diff --git a/templates/public/donate.html b/templates/public/donate.html index 08234006..ef80baea 100644 --- a/templates/public/donate.html +++ b/templates/public/donate.html @@ -44,20 +44,6 @@

    Commercial sponsors and contributions

    title="velocity network"> -

    We also wish to extend a special thank you to 7L Networks - for their generous and ongoing contribution of a dedicated Arch Linux - server. You too can have a dedicated Arch Linux server, or your server colocation service, - with 7L. Head over to their website for more details.

    - - -

    More thanks go to AirVM.com for contributing a VMWare-based Virtual Machine.

    diff --git a/templates/public/index.html b/templates/public/index.html index 2da03f4b..55298ae3 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -201,17 +201,12 @@

    More Resources

    {% endcache %} -- cgit v1.2.3-54-g00ecf From 92837c93acc66056391dd0b98515b89f8fc49691 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 12 Nov 2012 21:37:08 -0600 Subject: Prefetch the available protocols on the mirror overview page Otherwise we are doing one query per mirror, which at this point is over 100 separate queries. Signed-off-by: Dan McGee --- mirrors/models.py | 5 ----- mirrors/views.py | 8 ++++++++ templates/mirrors/mirrors.html | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/mirrors/models.py b/mirrors/models.py index 384668b8..0179d5bf 100644 --- a/mirrors/models.py +++ b/mirrors/models.py @@ -33,11 +33,6 @@ class Meta: def __unicode__(self): return self.name - def supported_protocols(self): - protocols = MirrorProtocol.objects.filter( - urls__mirror=self).order_by('protocol').distinct() - return sorted(protocols) - def downstream(self): return Mirror.objects.filter(upstream=self).order_by('name') diff --git a/mirrors/views.py b/mirrors/views.py index 5e374b4d..2e1e83b6 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -159,8 +159,16 @@ def find_mirrors_simple(request, protocol): def mirrors(request): mirror_list = Mirror.objects.select_related().order_by('tier', 'country') + protos = MirrorUrl.objects.values_list( + 'mirror_id', 'protocol__protocol').order_by( + 'mirror__id', 'protocol__protocol') if not request.user.is_authenticated(): mirror_list = mirror_list.filter(public=True, active=True) + protos = protos.filter(mirror__public=True, mirror__active=True) + protos = dict((k, list(v)) for k, v in groupby(protos, key=itemgetter(0))) + for mirror in mirror_list: + items = protos.get(mirror.id, []) + mirror.protocols = [item[1] for item in items] return render(request, 'mirrors/mirrors.html', {'mirror_list': mirror_list}) diff --git a/templates/mirrors/mirrors.html b/templates/mirrors/mirrors.html index 0950520d..c83d0d43 100644 --- a/templates/mirrors/mirrors.html +++ b/templates/mirrors/mirrors.html @@ -29,7 +29,7 @@

    Mirror Overview

    {{ mirror.get_tier_display }} {% if mirror.country %} {% endif %}{{ mirror.country.name }} {{ mirror.isos|yesno|capfirst }}{{ mirror.supported_protocols|join:", " }}{{ mirror.protocols|join:", " }}{{ mirror.public|yesno|capfirst }} {{ mirror.active|yesno|capfirst }}
    + + +
    +

    Master Key Signatures

    The following table shows all active developers and trusted users along with the status of their personal signing key. A 'Yes' indicates that the -- cgit v1.2.3-54-g00ecf From 59f5b6ed5524300cc0373ad934b251e220c66da9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 13 Nov 2012 09:51:11 -0600 Subject: Mirror details style and JS cleanup Signed-off-by: Dan McGee --- sitestatic/archweb.css | 9 +++++++++ templates/mirrors/mirror_details.html | 7 ++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index cef41399..52aec59f 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -314,6 +314,15 @@ table.pretty2 { border: 1px dotted #bbb; } +table.compact { + width: auto; +} + + table.compact td { + padding: 0.25em 0 0.25em 1.5em; + } + + /* definition lists */ dl { clear: both; diff --git a/templates/mirrors/mirror_details.html b/templates/mirrors/mirror_details.html index 32ef8a7a..884187b9 100644 --- a/templates/mirrors/mirror_details.html +++ b/templates/mirrors/mirror_details.html @@ -5,12 +5,11 @@ {% block title %}Arch Linux - {{ mirror.name }} - Mirror Details{% endblock %} {% block content %} - -

    +

    Mirror Details: {{ mirror.name }}

    - +
    @@ -118,6 +117,8 @@

    Mirror Status Chart

    $("#available_urls:has(tbody tr)").tablesorter( {widgets: ['zebra'], sortList: [[0,0]], headers: { 6: { sorter: 'mostlydigit' }, 7: { sorter: 'mostlydigit' }, 8: { sorter: 'mostlydigit' } } }); +}); +$(document).ready(function() { mirror_status("#visualize-mirror", "./json/"); }); -- cgit v1.2.3-54-g00ecf From 500d19a914eb13c310a1d2c7d28a1befce468419 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 13 Nov 2012 09:51:59 -0600 Subject: Move PGP key visualizations to master keys page Signed-off-by: Dan McGee --- templates/public/keys.html | 23 +++++++++++++++++++++-- templates/visualize/index.html | 5 ----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/templates/public/keys.html b/templates/public/keys.html index f23d1c40..81713efb 100644 --- a/templates/public/keys.html +++ b/templates/public/keys.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load static from staticfiles %} +{% load url from future %} {% load pgp %} {% block title %}Arch Linux - Master Signing Keys{% endblock %} @@ -47,10 +48,16 @@

    Master Signing Keys

    {% endfor %}
    Name: {{ mirror.name }}
    + +
    -

    Master Key Signatures

    +

    Master Key Signatures

    The following table shows all active developers and trusted users along with the status of their personal signing key. A 'Yes' indicates that the @@ -95,7 +102,13 @@

    Master Key Signatures

    -

    Developer Cross-Signatures

    +

    Visualization of PGP Master and Developer Keys

    + +
    +
    + +
    +

    Developer Cross-Signatures

    This table lists signatures directly between developer keys.

    @@ -120,8 +133,11 @@

    Developer Cross-Signatures

    + {% load cdn %}{% jquery %}{% jquery_tablesorter %} + + {% endblock %} diff --git a/templates/visualize/index.html b/templates/visualize/index.html index 95cf6c1c..242a7f0b 100644 --- a/templates/visualize/index.html +++ b/templates/visualize/index.html @@ -24,11 +24,7 @@

    Visualization of Package Data

    -
    -

    Visualization of PGP Master and Signing Keys

    -
    -
    {% load cdn %}{% jquery %} @@ -40,7 +36,6 @@

    Visualization of PGP Master and Signing Keys

    "arch": { url: "{% url 'visualize-byarch' %}", color_attr: "arch" }, }; packages_treemap("#visualize-archrepo", orderings, "repo"); - developer_keys("#visualize-keys", "{% url 'visualize-pgp_keys' %}"); }); {% endblock %} -- cgit v1.2.3-54-g00ecf From 5d468b2f7b42f71da7cbd475d20dd422f0bfffc5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 13 Nov 2012 09:53:12 -0600 Subject: Update rss.png Signed-off-by: Dan McGee --- sitestatic/rss.png | Bin 725 -> 707 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/sitestatic/rss.png b/sitestatic/rss.png index c9164592..a6f114cd 100644 Binary files a/sitestatic/rss.png and b/sitestatic/rss.png differ -- cgit v1.2.3-54-g00ecf From 45d81a9578e846062550335495dbceb82f16a1a0 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 13 Nov 2012 10:03:55 -0600 Subject: Move JSON keys view to public/ app This seems like a more appropriate place, and now the visualization is done here anyway so we should move the data backing it. Signed-off-by: Dan McGee --- public/views.py | 53 ++++++++++++++++++++++++++++++++++++++++++++-- templates/public/keys.html | 2 +- urls.py | 1 + visualize/urls.py | 1 - visualize/views.py | 51 +++++--------------------------------------- 5 files changed, 58 insertions(+), 50 deletions(-) diff --git a/public/views.py b/public/views.py index 42f2f345..96120761 100644 --- a/public/views.py +++ b/public/views.py @@ -1,12 +1,13 @@ from datetime import datetime +import json from operator import attrgetter from django.conf import settings from django.contrib.auth.models import User from django.db.models import Count, Q -from django.http import Http404 +from django.http import Http404, HttpResponse from django.shortcuts import render -from django.views.decorators.cache import cache_control +from django.views.decorators.cache import cache_control, cache_page from devel.models import MasterKey, PGPSignature from main.models import Arch, Repo, Donor @@ -14,6 +15,7 @@ from news.models import News from .utils import get_recent_updates + @cache_control(max_age=300) def index(request): if request.user.is_authenticated(): @@ -44,6 +46,7 @@ def index(request): }, } + @cache_control(max_age=300) def userlist(request, user_type='devs'): users = User.objects.order_by( @@ -63,6 +66,7 @@ def userlist(request, user_type='devs'): context['users'] = users return render(request, 'public/userlist.html', context) + @cache_control(max_age=300) def donate(request): context = { @@ -70,6 +74,7 @@ def donate(request): } return render(request, 'public/donate.html', context) + @cache_control(max_age=300) def download(request): mirror_urls = MirrorUrl.objects.select_related('mirror').filter( @@ -84,6 +89,7 @@ def download(request): } return render(request, 'public/download.html', context) + @cache_control(max_age=300) def feeds(request): repos = Repo.objects.all() @@ -95,6 +101,7 @@ def feeds(request): } return render(request, 'public/feeds.html', context) + @cache_control(max_age=300) def keys(request): users = User.objects.filter(is_active=True).select_related( @@ -132,4 +139,46 @@ def keys(request): } return render(request, 'public/keys.html', context) + +@cache_page(1800) +def keys_json(request): + node_list = [] + + users = User.objects.filter(is_active=True).select_related('userprofile') + node_list.extend({ + 'name': dev.get_full_name(), + 'key': dev.userprofile.pgp_key, + 'group': 'dev' + } for dev in users.filter(groups__name='Developers')) + node_list.extend({ + 'name': tu.get_full_name(), + 'key': tu.userprofile.pgp_key, + 'group': 'tu' + } for tu in users.filter(groups__name='Trusted Users').exclude( + groups__name='Developers')) + + master_keys = MasterKey.objects.select_related('owner').filter( + revoked__isnull=True) + node_list.extend({ + 'name': 'Master Key (%s)' % key.owner.get_full_name(), + 'key': key.pgp_key, + 'group': 'master' + } for key in master_keys) + + node_list.append({ + 'name': 'CA Cert Signing Authority', + 'key': 'A31D4F81EF4EBD07B456FA04D2BB0D0165D0FD58', + 'group': 'cacert', + }) + + not_expired = Q(expires__gt=datetime.utcnow) | Q(expires__isnull=True) + signatures = PGPSignature.objects.filter(not_expired, valid=True) + edge_list = [{ 'signee': sig.signee, 'signer': sig.signer } + for sig in signatures] + + data = { 'nodes': node_list, 'edges': edge_list } + + to_json = json.dumps(data, ensure_ascii=False) + return HttpResponse(to_json, mimetype='application/json') + # vim: set ts=4 sw=4 et: diff --git a/templates/public/keys.html b/templates/public/keys.html index 81713efb..9af491e2 100644 --- a/templates/public/keys.html +++ b/templates/public/keys.html @@ -149,7 +149,7 @@

    Developer Cross-Signatures

    }); }); $(document).ready(function() { - developer_keys("#visualize-keys", "{% url 'visualize-pgp_keys' %}"); + developer_keys("#visualize-keys", "{% url 'pgp-keys-json' %}"); }); {% endblock %} diff --git a/urls.py b/urls.py index fd5ff7cf..1bdf1a58 100644 --- a/urls.py +++ b/urls.py @@ -67,6 +67,7 @@ (r'^donate/$', 'donate', {}, 'page-donate'), (r'^download/$', 'download', {}, 'page-download'), (r'^master-keys/$', 'keys', {}, 'page-keys'), + (r'^master-keys/json/$', 'keys_json', {}, 'pgp-keys-json'), ) urlpatterns += patterns('retro.views', diff --git a/visualize/urls.py b/visualize/urls.py index 907f6a22..8c3ea06a 100644 --- a/visualize/urls.py +++ b/visualize/urls.py @@ -4,7 +4,6 @@ (r'^$', 'index', {}, 'visualize-index'), (r'^by_arch/$', 'by_arch', {}, 'visualize-byarch'), (r'^by_repo/$', 'by_repo', {}, 'visualize-byrepo'), - (r'^pgp_keys/$', 'pgp_keys', {}, 'visualize-pgp_keys'), ) # vim: set ts=4 sw=4 et: diff --git a/visualize/views.py b/visualize/views.py index 44e60472..8d878937 100644 --- a/visualize/views.py +++ b/visualize/views.py @@ -1,18 +1,17 @@ -from datetime import datetime import json -from django.contrib.auth.models import User -from django.db.models import Count, Sum, Q +from django.db.models import Count, Sum from django.http import HttpResponse from django.shortcuts import render from django.views.decorators.cache import cache_page from main.models import Package, Arch, Repo -from devel.models import MasterKey, PGPSignature + def index(request): return render(request, 'visualize/index.html') + def arch_repo_data(): qs = Package.objects.select_related().values( 'arch__name', 'repo__name').annotate( @@ -58,58 +57,18 @@ def build_map(name, arch, repo): } return data + @cache_page(1800) def by_arch(request): data = arch_repo_data() to_json = json.dumps(data['by_arch'], ensure_ascii=False) return HttpResponse(to_json, mimetype='application/json') + @cache_page(1800) def by_repo(request): data = arch_repo_data() to_json = json.dumps(data['by_repo'], ensure_ascii=False) return HttpResponse(to_json, mimetype='application/json') - -@cache_page(1800) -def pgp_keys(request): - node_list = [] - - users = User.objects.filter(is_active=True).select_related('userprofile') - node_list.extend({ - 'name': dev.get_full_name(), - 'key': dev.userprofile.pgp_key, - 'group': 'dev' - } for dev in users.filter(groups__name='Developers')) - node_list.extend({ - 'name': tu.get_full_name(), - 'key': tu.userprofile.pgp_key, - 'group': 'tu' - } for tu in users.filter(groups__name='Trusted Users').exclude( - groups__name='Developers')) - - master_keys = MasterKey.objects.select_related('owner').filter( - revoked__isnull=True) - node_list.extend({ - 'name': 'Master Key (%s)' % key.owner.get_full_name(), - 'key': key.pgp_key, - 'group': 'master' - } for key in master_keys) - - node_list.append({ - 'name': 'CA Cert Signing Authority', - 'key': 'A31D4F81EF4EBD07B456FA04D2BB0D0165D0FD58', - 'group': 'cacert', - }) - - not_expired = Q(expires__gt=datetime.utcnow) | Q(expires__isnull=True) - signatures = PGPSignature.objects.filter(not_expired, valid=True) - edge_list = [{ 'signee': sig.signee, 'signer': sig.signer } - for sig in signatures] - - data = { 'nodes': node_list, 'edges': edge_list } - - to_json = json.dumps(data, ensure_ascii=False) - return HttpResponse(to_json, mimetype='application/json') - # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From a2cfa7edbbed8edb1ad4d3391c6edb055c13de1b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 16 Nov 2012 15:39:56 -0600 Subject: Optimize mirror status data fetching Now that we have as many mirror URLs as we do, we can do a better job fetching and aggregating this data. The prior method resulted in a rather unwieldy query being pushed down to the database with a horrendously long GROUP BY clause. Instead of trying to group by everything at once so we can retrieve mirror URL info at the same time, separate the two queries- one for getting URL performance data, one for the qualitative data. The impetus behind fixing this is the PostgreSQL slow query log in production; this currently shows up the most of any queries we run in the system. Signed-off-by: Dan McGee --- mirrors/utils.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/mirrors/utils.py b/mirrors/utils.py index 85e4ee93..07a7138f 100644 --- a/mirrors/utils.py +++ b/mirrors/utils.py @@ -33,23 +33,26 @@ def annotate_url(url, delays): @cache_function(123) def get_mirror_statuses(cutoff=DEFAULT_CUTOFF, mirror_ids=None): cutoff_time = now() - cutoff - # I swear, this actually has decent performance... - urls = MirrorUrl.objects.select_related('mirror', 'protocol').filter( + url_data = MirrorUrl.objects.values('id', 'mirror_id').filter( mirror__active=True, mirror__public=True, logs__check_time__gte=cutoff_time).annotate( check_count=Count('logs'), success_count=Count('logs__duration'), last_sync=Max('logs__last_sync'), last_check=Max('logs__check_time'), - duration_avg=Avg('logs__duration')).order_by( - 'mirror', 'url') - - if mirror_ids: - urls = urls.filter(mirror_id__in=mirror_ids) + duration_avg=Avg('logs__duration')) vendor = database_vendor(MirrorUrl) if vendor != 'sqlite': - urls = urls.annotate(duration_stddev=StdDev('logs__duration')) + url_data = url_data.annotate(duration_stddev=StdDev('logs__duration')) + + urls = MirrorUrl.objects.select_related('mirror', 'protocol').filter( + mirror__active=True, mirror__public=True, + logs__check_time__gte=cutoff_time).order_by('mirror__id', 'url') + + if mirror_ids: + url_data = url_data.filter(mirror_id__in=mirror_ids) + urls = urls.filter(mirror_id__in=mirror_ids) # The Django ORM makes it really hard to get actual average delay in the # above query, so run a seperate query for it and we will process the @@ -66,6 +69,11 @@ def get_mirror_statuses(cutoff=DEFAULT_CUTOFF, mirror_ids=None): delays.setdefault(url_id, []).append(delay) if urls: + url_data = dict((item['id'], item) for item in url_data) + for url in urls: + for k, v in url_data.get(url.id, {}).items(): + if k not in ('id', 'mirror_id'): + setattr(url, k, v) last_check = max([u.last_check for u in urls]) num_checks = max([u.check_count for u in urls]) check_info = MirrorLog.objects.filter(check_time__gte=cutoff_time) -- cgit v1.2.3-54-g00ecf From b3979dd2315ce1aef779abdb646562d7bba7a44f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 16 Nov 2012 15:49:42 -0600 Subject: Bump several virtualenv requirements Signed-off-by: Dan McGee --- requirements.txt | 6 +++--- requirements_prod.txt | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 51f15a0f..cf5cacb3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Django==1.4.2 -Markdown==2.2.0 +Markdown==2.2.1 South==0.7.6 django-countries==1.4 -pgpdump==1.3 -pytz>=2012f +pgpdump==1.4 +pytz>=2012h diff --git a/requirements_prod.txt b/requirements_prod.txt index 2a13c51a..f11b9119 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -1,9 +1,9 @@ Django==1.4.2 -Markdown==2.2.0 +Markdown==2.2.1 South==0.7.6 django-countries==1.4 -pgpdump==1.3 +pgpdump==1.4 psycopg2==2.4.5 -pyinotify==0.9.3 +pyinotify==0.9.4 python-memcached==1.48 -pytz>=2012f +pytz>=2012h -- cgit v1.2.3-54-g00ecf From 6dd4d54bb0adbbb0f8c2b1beaa92b7a58971cf88 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 16 Nov 2012 16:20:11 -0600 Subject: Use Python 2.7 dictionary comprehension syntax Rather than the old idiom of dict((k, v) for <> in <>). Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 4 ++-- devel/management/commands/reporead_inotify.py | 2 +- mirrors/views.py | 11 ++++------- packages/templatetags/package_extras.py | 2 +- packages/utils.py | 5 ++--- packages/views/signoff.py | 8 +++----- public/views.py | 2 +- visualize/views.py | 4 ++-- 8 files changed, 16 insertions(+), 22 deletions(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index a1e77b49..3d4e6375 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -377,7 +377,7 @@ def db_update(archname, reponame, pkgs, force=False): # This makes our inner loop where we find packages by name *way* more # efficient by not having to go to the database for each package to # SELECT them by name. - dbdict = dict((dbpkg.pkgname, dbpkg) for dbpkg in dbpkgs) + dbdict = {dbpkg.pkgname: dbpkg for dbpkg in dbpkgs} dbset = set(dbdict.keys()) syncset = set([pkg.name for pkg in pkgs]) @@ -446,7 +446,7 @@ def filesonly_update(archname, reponame, pkgs, force=False): """ logger.info('Updating files for %s (%s)', reponame, archname) dbpkgs = update_common(archname, reponame, pkgs, False) - dbdict = dict((dbpkg.pkgname, dbpkg) for dbpkg in dbpkgs) + dbdict = {dbpkg.pkgname: dbpkg for dbpkg in dbpkgs} dbset = set(dbdict.keys()) for pkg in (pkg for pkg in pkgs if pkg.name in dbset): diff --git a/devel/management/commands/reporead_inotify.py b/devel/management/commands/reporead_inotify.py index c74762eb..16b3869c 100644 --- a/devel/management/commands/reporead_inotify.py +++ b/devel/management/commands/reporead_inotify.py @@ -69,7 +69,7 @@ def setup_notifier(self): finally builds and returns a notifier object.''' arches = Arch.objects.filter(agnostic=False) repos = Repo.objects.all() - arch_path_map = dict((arch, None) for arch in arches) + arch_path_map = {arch: None for arch in arches} all_paths = set() total_paths = 0 for arch in arches: diff --git a/mirrors/views.py b/mirrors/views.py index 2e1e83b6..d0ce0a97 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -94,7 +94,7 @@ def default_protocol_filter(original_urls): def status_filter(original_urls): status_info = get_mirror_statuses() - scores = dict((u.id, u.score) for u in status_info['urls']) + scores = {u.id: u.score for u in status_info['urls']} urls = [] for u in original_urls: u.score = scores.get(u.id, None) @@ -165,7 +165,7 @@ def mirrors(request): if not request.user.is_authenticated(): mirror_list = mirror_list.filter(public=True, active=True) protos = protos.filter(mirror__public=True, mirror__active=True) - protos = dict((k, list(v)) for k, v in groupby(protos, key=itemgetter(0))) + protos = {k: list(v) for k, v in groupby(protos, key=itemgetter(0))} for mirror in mirror_list: items = protos.get(mirror.id, []) mirror.protocols = [item[1] for item in items] @@ -253,8 +253,7 @@ def default(self, obj): # mainly for queryset serialization return list(obj) if isinstance(obj, MirrorUrl): - data = dict((attr, getattr(obj, attr)) - for attr in self.url_attributes) + data = {attr: getattr(obj, attr) for attr in self.url_attributes} # get any override on the country attribute first country = obj.real_country data['country'] = unicode(country.name) @@ -277,9 +276,7 @@ def default(self, obj): check_time__gte=cutoff).order_by('check_time') return data if isinstance(obj, MirrorLog): - data = dict((attr, getattr(obj, attr)) - for attr in self.log_attributes) - return data + return {attr: getattr(obj, attr) for attr in self.log_attributes} return super(ExtendedMirrorStatusJSONEncoder, self).default(obj) diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py index 994265d8..f3613e69 100644 --- a/packages/templatetags/package_extras.py +++ b/packages/templatetags/package_extras.py @@ -13,7 +13,7 @@ def link_encode(url, query): # massage the data into all utf-8 encoded strings first, so urlencode # doesn't barf at the data we pass it - query = dict((k, unicode(v).encode('utf-8')) for k, v in query.items()) + query = {k: unicode(v).encode('utf-8') for k, v in query.items()} data = urlencode(query).replace('&', '&') return "%s?%s" % (url, data) diff --git a/packages/utils.py b/packages/utils.py index 051fed8e..199e141d 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -385,7 +385,7 @@ def signoffs_id_query(model, repos): repo_sql = ','.join(['%s' for r in repos]) sql = sql % (model._meta.db_table, repo_sql, repo_sql) repo_ids = [r.pk for r in repos] - # repo_ids are needed twice, so double the array + # repo_ids are needed twice, so double the array cursor.execute(sql, repo_ids * 2) results = cursor.fetchall() @@ -474,8 +474,7 @@ def default(self, obj): # mainly for queryset serialization return list(obj) if isinstance(obj, Package): - data = dict((attr, getattr(obj, attr)) - for attr in self.pkg_attributes) + data = {attr: getattr(obj, attr) for attr in self.pkg_attributes} for attr in self.pkg_list_attributes: data[attr] = getattr(obj, attr).all() return data diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 56eb060c..824a9922 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -155,8 +155,8 @@ class SignoffJSONEncoder(DjangoJSONEncoder): def default(self, obj): if isinstance(obj, PackageSignoffGroup): - data = dict((attr, getattr(obj, attr)) - for attr in self.signoff_group_attrs) + data = {attr: getattr(obj, attr) + for attr in self.signoff_group_attrs} data['pkgnames'] = [p.pkgname for p in obj.packages] data['package_count'] = len(obj.packages) data['approved'] = obj.approved() @@ -164,9 +164,7 @@ def default(self, obj): for attr in self.signoff_spec_attrs) return data elif isinstance(obj, Signoff): - data = dict((attr, getattr(obj, attr)) - for attr in self.signoff_attrs) - return data + return {attr: getattr(obj, attr) for attr in self.signoff_attrs} elif isinstance(obj, Arch) or isinstance(obj, Repo): return unicode(obj) elif isinstance(obj, User): diff --git a/public/views.py b/public/views.py index 96120761..3e15f9df 100644 --- a/public/views.py +++ b/public/views.py @@ -118,7 +118,7 @@ def keys(request): sig_counts = PGPSignature.objects.filter(not_expired, valid=True, signee__in=user_key_ids).values_list('signer').annotate( Count('signer')) - sig_counts = dict((key_id[-16:], ct) for key_id, ct in sig_counts) + sig_counts = {key_id[-16:]: ct for key_id, ct in sig_counts} for key in master_keys: key.signature_count = sig_counts.get(key.pgp_key[-16:], 0) diff --git a/visualize/views.py b/visualize/views.py index 8d878937..48e8f86b 100644 --- a/visualize/views.py +++ b/visualize/views.py @@ -33,8 +33,8 @@ def build_map(name, arch, repo): # now transform these results into two mappings: one ordered (repo, arch), # and one ordered (arch, repo). - arch_groups = dict((a, build_map(a, a, None)) for a in arches) - repo_groups = dict((r, build_map(r, None, r)) for r in repos) + arch_groups = {a: build_map(a, a, None) for a in arches} + repo_groups = {r: build_map(r, None, r) for r in repos} for row in qs: arch = row['arch__name'] repo = row['repo__name'] -- cgit v1.2.3-54-g00ecf From 9e9157d0a8cbf9ea076231e438fb30f58bff8e29 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 16 Nov 2012 16:37:31 -0600 Subject: Use python set comprehension syntax supported in 2.7 Signed-off-by: Dan McGee --- devel/management/commands/import_signatures.py | 4 ++-- devel/management/commands/reporead.py | 2 +- devel/management/commands/reporead_inotify.py | 2 +- devel/views.py | 4 ++-- main/models.py | 2 +- packages/models.py | 2 +- packages/utils.py | 10 +++++----- packages/views/signoff.py | 2 +- todolists/views.py | 8 ++++---- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/devel/management/commands/import_signatures.py b/devel/management/commands/import_signatures.py index ce1aba90..da1397ca 100644 --- a/devel/management/commands/import_signatures.py +++ b/devel/management/commands/import_signatures.py @@ -98,8 +98,8 @@ def import_signatures(keyring): # now prune the data down to what we actually want. # prune edges not in nodes, remove duplicates, and self-sigs - pruned_edges = set(edge for edge in edges - if edge.signer in nodes and edge.signer != edge.signee) + pruned_edges = {edge for edge in edges + if edge.signer in nodes and edge.signer != edge.signee} logger.info("creating or finding %d signatures", len(pruned_edges)) created_ct = updated_ct = 0 diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index 3d4e6375..981c4dce 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -380,7 +380,7 @@ def db_update(archname, reponame, pkgs, force=False): dbdict = {dbpkg.pkgname: dbpkg for dbpkg in dbpkgs} dbset = set(dbdict.keys()) - syncset = set([pkg.name for pkg in pkgs]) + syncset = {pkg.name for pkg in pkgs} in_sync_not_db = syncset - dbset logger.info("%d packages in sync not db", len(in_sync_not_db)) diff --git a/devel/management/commands/reporead_inotify.py b/devel/management/commands/reporead_inotify.py index 16b3869c..04f65764 100644 --- a/devel/management/commands/reporead_inotify.py +++ b/devel/management/commands/reporead_inotify.py @@ -77,7 +77,7 @@ def setup_notifier(self): for repo in repos) # take a python format string and generate all unique combinations # of directories from it; using set() ensures we filter it down - paths = set(self.path_template % values for values in combos) + paths = {self.path_template % values for values in combos} total_paths += len(paths) all_paths |= paths arch_path_map[arch] = paths diff --git a/devel/views.py b/devel/views.py index 083665d9..7d5947d1 100644 --- a/devel/views.py +++ b/devel/views.py @@ -277,8 +277,8 @@ def report(request, report_name, username=None): else: raise Http404 - arches = set(pkg.arch for pkg in packages) - repos = set(pkg.repo for pkg in packages) + arches = {pkg.arch for pkg in packages} + repos = {pkg.repo for pkg in packages} context = { 'all_maintainers': maints, 'title': title, diff --git a/main/models.py b/main/models.py index 5700cdf1..cc81637c 100644 --- a/main/models.py +++ b/main/models.py @@ -197,7 +197,7 @@ def get_requiredby(self): """ from packages.models import Depend provides = self.provides.all() - provide_names = set(provide.name for provide in provides) + provide_names = {provide.name for provide in provides} provide_names.add(self.pkgname) requiredby = Depend.objects.select_related('pkg', 'pkg__arch', 'pkg__repo').filter( diff --git a/packages/models.py b/packages/models.py index 0d0fbdf2..ede8c275 100644 --- a/packages/models.py +++ b/packages/models.py @@ -33,7 +33,7 @@ def get_associated_packages(self): def repositories(self): packages = self.get_associated_packages() - return sorted(set([p.repo for p in packages])) + return sorted({p.repo for p in packages}) def __unicode__(self): return u'%s: %s (%s)' % ( diff --git a/packages/utils.py b/packages/utils.py index 199e141d..5adc8637 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -79,8 +79,8 @@ def get_split_packages_info(): split_pkgs = Package.objects.exclude(pkgname=F('pkgbase')).exclude( pkgbase__in=pkgnames).values('pkgbase', 'repo', 'arch').annotate( last_update=Max('last_update')) - all_arches = Arch.objects.in_bulk(set(s['arch'] for s in split_pkgs)) - all_repos = Repo.objects.in_bulk(set(s['repo'] for s in split_pkgs)) + all_arches = Arch.objects.in_bulk({s['arch'] for s in split_pkgs}) + all_repos = Repo.objects.in_bulk({s['repo'] for s in split_pkgs}) for split in split_pkgs: split['arch'] = all_arches[split['arch']] split['repo'] = all_repos[split['repo']] @@ -143,7 +143,7 @@ def get_differences_info(arch_a, arch_b): cursor.execute(sql, [arch_a.id, arch_b.id]) results = cursor.fetchall() # column A will always have a value, column B might be NULL - to_fetch = set(row[0] for row in results) + to_fetch = {row[0] for row in results} # fetch all of the necessary packages pkgs = Package.objects.normal().in_bulk(to_fetch) # now build a list of tuples containing differences @@ -249,13 +249,13 @@ def attach_maintainers(packages): the maintainers and attach them to the packages to prevent N+1 query cascading.''' packages = list(packages) - pkgbases = set(p.pkgbase for p in packages) + pkgbases = {p.pkgbase for p in packages} rels = PackageRelation.objects.filter(type=PackageRelation.MAINTAINER, pkgbase__in=pkgbases).values_list( 'pkgbase', 'user_id').order_by().distinct() # get all the user objects we will need - user_ids = set(rel[1] for rel in rels) + user_ids = {rel[1] for rel in rels} users = User.objects.in_bulk(user_ids) # now build a pkgbase -> [maintainers...] map diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 824a9922..340b2311 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -25,7 +25,7 @@ def signoffs(request): context = { 'signoff_groups': signoff_groups, 'arches': Arch.objects.all(), - 'repo_names': sorted(set(g.target_repo for g in signoff_groups)), + 'repo_names': sorted({g.target_repo for g in signoff_groups}), } return render(request, 'packages/signoffs.html', context) diff --git a/todolists/views.py b/todolists/views.py index b8d1dae1..9984ef9a 100644 --- a/todolists/views.py +++ b/todolists/views.py @@ -53,8 +53,8 @@ def view(request, list_id): # we don't hold onto the result, but the objects are the same here, # so accessing maintainers in the template is now cheap attach_maintainers(tp.pkg for tp in todolist.packages) - arches = set(tp.pkg.arch for tp in todolist.packages) - repos = set(tp.pkg.repo for tp in todolist.packages) + arches = {tp.pkg.arch for tp in todolist.packages} + repos = {tp.pkg.repo for tp in todolist.packages} return render(request, 'todolists/view.html', { 'list': todolist, 'svn_roots': svn_roots, @@ -67,8 +67,8 @@ def list_pkgbases(request, list_id, svn_root): '''Used to make bulk moves of packages a lot easier.''' todolist = get_object_or_404(Todolist, id=list_id) repos = get_list_or_404(Repo, svn_root=svn_root) - pkgbases = set(tp.pkg.pkgbase for tp in todolist.packages - if tp.pkg.repo in repos) + pkgbases = {tp.pkg.pkgbase for tp in todolist.packages + if tp.pkg.repo in repos} return HttpResponse('\n'.join(sorted(pkgbases)), mimetype='text/plain') -- cgit v1.2.3-54-g00ecf From 160a08bba5324b25abd9e866b884c91d75e597b0 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 20 Nov 2012 15:57:05 -0600 Subject: Improve performance of todolists query Use some standard SQL and split the query into two different parts to save a lot of unnecessary sorting and field retrieval at the database level. The `CASE WHEN complete THEN 1 ELSE 0 END` syntax should be accepted by any database that implements proper SQL; it was tested in PostgreSQL and sqlite3 without issues. Signed-off-by: Dan McGee --- todolists/utils.py | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/todolists/utils.py b/todolists/utils.py index 94f39f71..03c47931 100644 --- a/todolists/utils.py +++ b/todolists/utils.py @@ -1,26 +1,37 @@ +from django.db import connections, router from django.db.models import Count -from main.models import Todolist +from main.models import Todolist, TodolistPkg -def get_annotated_todolists(incomplete_only=False): - qs = Todolist.objects.all() - lists = qs.select_related('creator').defer( - 'creator__email', 'creator__password', 'creator__is_staff', - 'creator__is_active', 'creator__is_superuser', - 'creator__last_login', 'creator__date_joined').annotate( - pkg_count=Count('todolistpkg')).order_by('-date_added') - incomplete = qs.filter(todolistpkg__complete=False).annotate( - Count('todolistpkg')).values_list('id', 'todolistpkg__count') +def todo_counts(): + sql = """ +SELECT list_id, count(*), sum(CASE WHEN complete THEN 1 ELSE 0 END) + FROM todolist_pkgs + GROUP BY list_id + """ + database = router.db_for_write(TodolistPkg) + connection = connections[database] + cursor = connection.cursor() + cursor.execute(sql) + results = cursor.fetchall() + return {row[0]: (row[1], row[2]) for row in results} - lookup = dict(incomplete) - if incomplete_only: - lists = lists.filter(id__in=lookup.keys()) +def get_annotated_todolists(incomplete_only=False): + lists = Todolist.objects.all().select_related( + 'creator').order_by('-date_added') + lookup = todo_counts() - # tag each list with an incomplete package count + # tag each list with package counts for todolist in lists: - todolist.incomplete_count = lookup.get(todolist.id, 0) + counts = lookup.get(todolist.id, (0, 0)) + todolist.pkg_count = counts[0] + todolist.complete_count = counts[1] + todolist.incomplete_count = counts[0] - counts[1] + + if incomplete_only: + lists = [l for l in lists if l.incomplete_count > 0] return lists -- cgit v1.2.3-54-g00ecf From f7331a0eca351300685ebee494e810d8c82c35b1 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 20 Nov 2012 19:16:25 -0600 Subject: Add Release model to releng This should prevent the need for monthly template updates from Pierre and Thomas; best to just let them enter the data themselves and have it show up on the website. Signed-off-by: Dan McGee --- public/views.py | 6 ++ releng/admin.py | 23 +++--- releng/migrations/0004_auto__add_release.py | 121 ++++++++++++++++++++++++++++ releng/models.py | 19 ++++- templates/public/download.html | 22 +++-- 5 files changed, 167 insertions(+), 24 deletions(-) create mode 100644 releng/migrations/0004_auto__add_release.py diff --git a/public/views.py b/public/views.py index 3e15f9df..fefe032e 100644 --- a/public/views.py +++ b/public/views.py @@ -13,6 +13,7 @@ from main.models import Arch, Repo, Donor from mirrors.models import MirrorUrl from news.models import News +from releng.models import Release from .utils import get_recent_updates @@ -77,12 +78,17 @@ def donate(request): @cache_control(max_age=300) def download(request): + try: + release = Release.objects.filter(available=True).latest() + except Release.DoesNotExist: + release = None mirror_urls = MirrorUrl.objects.select_related('mirror').filter( protocol__default=True, mirror__public=True, mirror__active=True, mirror__isos=True) sort_by = attrgetter('real_country.name', 'mirror.name') mirror_urls = sorted(mirror_urls, key=sort_by) context = { + 'release': release, 'releng_iso_url': settings.ISO_LIST_URL, 'releng_pxeboot_url': settings.PXEBOOT_URL, 'mirror_urls': mirror_urls, diff --git a/releng/admin.py b/releng/admin.py index 42755002..c7e6396e 100644 --- a/releng/admin.py +++ b/releng/admin.py @@ -2,7 +2,7 @@ from .models import (Architecture, BootType, Bootloader, ClockChoice, Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source, - Test) + Test, Release) class IsoAdmin(admin.ModelAdmin): list_display = ('name', 'created', 'active', 'removed') @@ -14,19 +14,20 @@ class TestAdmin(admin.ModelAdmin): 'iso', 'success') list_filter = ('success', 'iso') +class ReleaseAdmin(admin.ModelAdmin): + list_display = ('version', 'release_date', 'kernel_version', 'available', + 'created') + list_filter = ('available', 'release_date') -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) + +SIMPLE_MODELS = (Architecture, BootType, Bootloader, ClockChoice, Filesystem, + HardwareType, InstallType, IsoType, Module, Source) + +for model in SIMPLE_MODELS: + admin.site.register(model) admin.site.register(Iso, IsoAdmin) admin.site.register(Test, TestAdmin) +admin.site.register(Release, ReleaseAdmin) # vim: set ts=4 sw=4 et: diff --git a/releng/migrations/0004_auto__add_release.py b/releng/migrations/0004_auto__add_release.py new file mode 100644 index 00000000..fe4acea5 --- /dev/null +++ b/releng/migrations/0004_auto__add_release.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.create_table('releng_release', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('release_date', self.gf('django.db.models.fields.DateField')(db_index=True)), + ('version', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('kernel_version', self.gf('django.db.models.fields.CharField')(max_length=50, blank=True)), + ('torrent_infohash', self.gf('django.db.models.fields.CharField')(max_length=64, blank=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')()), + ('available', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('info', self.gf('django.db.models.fields.TextField')(blank=True)), + )) + db.send_create_signal('releng', ['Release']) + + def backwards(self, orm): + db.delete_table('releng_release') + + + 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'}), + 'removed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}) + }, + '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.release': { + 'Meta': {'ordering': "('-release_date', '-version')", 'object_name': 'Release'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'kernel_version': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'release_date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}), + 'torrent_infohash': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + '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.GenericIPAddressField', [], {'max_length': '39'}), + '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/models.py b/releng/models.py index bd178add..2f9a0785 100644 --- a/releng/models.py +++ b/releng/models.py @@ -105,7 +105,24 @@ class Test(models.Model): comments = models.TextField(null=True, blank=True) -for model in (Iso, Test): +class Release(models.Model): + release_date = models.DateField(db_index=True) + version = models.CharField(max_length=50) + kernel_version = models.CharField(max_length=50, blank=True) + torrent_infohash = models.CharField(max_length=64, blank=True) + created = models.DateTimeField(editable=False) + available = models.BooleanField(default=True) + info = models.TextField('Public information', blank=True) + + class Meta: + get_latest_by = 'release_date' + ordering = ('-release_date', '-version') + + def __unicode__(self): + return self.version + + +for model in (Iso, Test, Release): pre_save.connect(set_created_field, sender=model, dispatch_uid="releng.models") diff --git a/templates/public/download.html b/templates/public/download.html index 2fddd4e9..d0754e5b 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -7,7 +7,6 @@ {% block navbarclass %}anb-download{% endblock %} {% block content %} -{% with version="2012.11.01" kernel_version="3.6.4" torrent_infohash="f86f84c74edc90336f94f0837afa3071ada2aaa8" %}

    Arch Linux Downloads

    @@ -20,8 +19,8 @@

    Release Info

    can always be updated with `pacman -Syu`.

    -{% endwith %} {% endblock %} -- cgit v1.2.3-54-g00ecf From 402487b007e206b013ecbf8b3017dc1231f4bbbc Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 20 Nov 2012 19:33:49 -0600 Subject: Move some logic out of the templates to the Release model This includes magnet URI generation, ISO paths, etc. Signed-off-by: Dan McGee --- releng/models.py | 18 ++++++++++++++++++ templates/public/download.html | 16 ++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/releng/models.py b/releng/models.py index 2f9a0785..c591bc0f 100644 --- a/releng/models.py +++ b/releng/models.py @@ -1,3 +1,5 @@ +from urllib import urlencode + from django.core.urlresolvers import reverse from django.db import models from django.db.models.signals import pre_save @@ -121,6 +123,22 @@ class Meta: def __unicode__(self): return self.version + def dir_path(self): + return "iso/%s/" % self.version + + def iso_url(self): + return "iso/%s/archlinux-%s-dual.iso" % (self.version, self.version) + + def magnet_uri(self): + query = { + 'dn': "archlinux-%s-dual.iso" % self.version, + 'tr': ("udp://tracker.archlinux.org:6969", + "http://tracker.archlinux.org:6969/announce"), + } + if self.torrent_infohash: + query['xt'] = "urn:btih:%s" % self.torrent_infohash + return "magnet:?%s" % urlencode(query, doseq=True) + for model in (Iso, Test, Release): pre_save.connect(set_created_field, sender=model, diff --git a/templates/public/download.html b/templates/public/download.html index d0754e5b..5733ee94 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -44,9 +44,9 @@

    BitTorrent Download (recommended)

    download is finished, so you can seed it back to others. A web-seed capable client is recommended for fastest download speeds.

    Netboot

    @@ -69,11 +69,11 @@

    Checksums

    File integrity checksums for the latest releases can be found below:

    @@ -85,8 +85,8 @@

    Checksums

    {% else %}
    Worldwide
    {% endif %} {% endfor %} -- cgit v1.2.3-54-g00ecf From c81a9271b8bbc03418442c01d50a4c4945999e71 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 21 Nov 2012 00:10:12 -0500 Subject: Show release notes on downloads page Signed-off-by: Dan McGee --- releng/models.py | 6 ++++++ templates/public/download.html | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/releng/models.py b/releng/models.py index c591bc0f..b3ab1a31 100644 --- a/releng/models.py +++ b/releng/models.py @@ -1,8 +1,10 @@ +import markdown from urllib import urlencode from django.core.urlresolvers import reverse from django.db import models from django.db.models.signals import pre_save +from django.utils.safestring import mark_safe from main.utils import set_created_field @@ -139,6 +141,10 @@ def magnet_uri(self): query['xt'] = "urn:btih:%s" % self.torrent_infohash return "magnet:?%s" % urlencode(query, doseq=True) + def info_html(self): + return mark_safe(markdown.markdown( + self.info, safe_mode=True, enable_attributes=False)) + for model in (Iso, Test, Release): pre_save.connect(set_created_field, sender=model, diff --git a/templates/public/download.html b/templates/public/download.html index 5733ee94..5f6f2d02 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -32,6 +32,12 @@

    Release Info

    + {% if release.info %} +

    Release Notes

    + +
    {{ release.info_html }}
    + {% endif %} +

    Existing Arch Users

    If you are an existing Arch user, there is no need to download a new ISO -- cgit v1.2.3-54-g00ecf From 9db1d4c6a0c60f32628dbd8d217fbd6c3ba99509 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 21 Nov 2012 00:19:23 -0500 Subject: Minor download template tweaks Signed-off-by: Dan McGee --- templates/public/download.html | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/templates/public/download.html b/templates/public/download.html index 5f6f2d02..3005ffb3 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -20,7 +20,7 @@

    Release Info

    • Current Release: {{ release.version }}
    • -
    • Included Kernel: {{ release.kernel_version }}
    • + {% if release.kernel_version %}
    • Included Kernel: {{ release.kernel_version }}
    • {% endif %}
    • Installation Guide
    • Resources:
        @@ -49,19 +49,15 @@

        BitTorrent Download (recommended)

        If you can spare the bytes, please leave the client open after your download is finished, so you can seed it back to others. A web-seed capable client is recommended for fastest download speeds.

        - +

        Download torrent for {{ release.version }} + (Magnet)

        Netboot

        If you have a wired connection, you can boot the latest release directly over the network.

        - +

        Arch Linux Netboot

        HTTP Direct Downloads

        -- cgit v1.2.3-54-g00ecf From f0f6f7235a62186c1cae9c79036dde5d8821373d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 19 Nov 2012 08:10:21 -0600 Subject: Fix mirror URL duplication in status view We need to ensure we don't duplicate URLs in the status view, so add a distinct() call back in to the queryset when it was inadvertently dropped in commit a2cfa7edbb. This negates a lot of the performance gains we had, unfortunately, so it looks like a nested subquery might be more efficient. Disappointing the planner can't do this for us. Signed-off-by: Dan McGee --- mirrors/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mirrors/utils.py b/mirrors/utils.py index 07a7138f..a62c7f05 100644 --- a/mirrors/utils.py +++ b/mirrors/utils.py @@ -48,7 +48,8 @@ def get_mirror_statuses(cutoff=DEFAULT_CUTOFF, mirror_ids=None): urls = MirrorUrl.objects.select_related('mirror', 'protocol').filter( mirror__active=True, mirror__public=True, - logs__check_time__gte=cutoff_time).order_by('mirror__id', 'url') + logs__check_time__gte=cutoff_time).distinct().order_by( + 'mirror__id', 'url') if mirror_ids: url_data = url_data.filter(mirror_id__in=mirror_ids) -- cgit v1.2.3-54-g00ecf From 94bb06971d115ea1b91e2461bc94fbd0fe948ff5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 20 Nov 2012 23:29:38 -0600 Subject: Bump django-countries requirement Signed-off-by: Dan McGee --- requirements.txt | 2 +- requirements_prod.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index cf5cacb3..97fbaf08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Django==1.4.2 Markdown==2.2.1 South==0.7.6 -django-countries==1.4 +django-countries==1.5 pgpdump==1.4 pytz>=2012h diff --git a/requirements_prod.txt b/requirements_prod.txt index f11b9119..04fdf0eb 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -1,7 +1,7 @@ Django==1.4.2 Markdown==2.2.1 South==0.7.6 -django-countries==1.4 +django-countries==1.5 pgpdump==1.4 psycopg2==2.4.5 pyinotify==0.9.4 -- cgit v1.2.3-54-g00ecf From a732d3cebcb8ff3170502b13d01ba90ac8efe26f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 21 Nov 2012 00:54:20 -0500 Subject: Fix new magnet link generation Apparently clients don't like urlencoded values in the magnet link, so %3A isn't treated the same as ':'. Signed-off-by: Dan McGee --- releng/models.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/releng/models.py b/releng/models.py index b3ab1a31..a22e01d6 100644 --- a/releng/models.py +++ b/releng/models.py @@ -1,5 +1,4 @@ import markdown -from urllib import urlencode from django.core.urlresolvers import reverse from django.db import models @@ -132,14 +131,14 @@ def iso_url(self): return "iso/%s/archlinux-%s-dual.iso" % (self.version, self.version) def magnet_uri(self): - query = { - 'dn': "archlinux-%s-dual.iso" % self.version, - 'tr': ("udp://tracker.archlinux.org:6969", - "http://tracker.archlinux.org:6969/announce"), - } + query = [ + ('dn', "archlinux-%s-dual.iso" % self.version), + ('tr', "udp://tracker.archlinux.org:6969"), + ('tr', "http://tracker.archlinux.org:6969/announce"), + ] if self.torrent_infohash: - query['xt'] = "urn:btih:%s" % self.torrent_infohash - return "magnet:?%s" % urlencode(query, doseq=True) + query.insert(0, ('xt', "urn:btih:%s" % self.torrent_infohash)) + return "magnet:?%s" % '&'.join(['%s=%s' % (k, v) for k, v in query]) def info_html(self): return mark_safe(markdown.markdown( -- cgit v1.2.3-54-g00ecf From eea25558c766d5f3a32879d16e579d051906cbf3 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 27 Nov 2012 08:48:01 -0600 Subject: Don't cache package properties as aggressively For package signatures, it turns out it is way cheaper to just parse the signature again rather than going though all the decorator and cache_function_key business. This speeds up the mismatched signatures report significantly once this is removed. For base_package, given that we only call it once from our package details template, it makes little sense to cache the result. Signed-off-by: Dan McGee --- main/models.py | 3 --- templates/packages/details.html | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/main/models.py b/main/models.py index cc81637c..603d7ccc 100644 --- a/main/models.py +++ b/main/models.py @@ -141,7 +141,6 @@ def get_full_url(self, proto='https'): return '%s://%s%s' % (proto, domain, self.get_absolute_url()) @property - @cache_function(15) def signature(self): try: data = b64decode(self.pgp_signature) @@ -154,7 +153,6 @@ def signature(self): return packets[0] @property - @cache_function(15) def signer(self): sig = self.signature if sig and sig.key_id: @@ -318,7 +316,6 @@ def reverse_conflicts(self): new_pkgs.append(package) return new_pkgs - @cache_function(125) def base_package(self): """ Locate the base package for this package. It may be this very package, diff --git a/templates/packages/details.html b/templates/packages/details.html index aa073551..0a47217c 100644 --- a/templates/packages/details.html +++ b/templates/packages/details.html @@ -103,12 +103,12 @@

        Versions Elsewhere

        {% else %} Base Package: - {% if pkg.base_package %} - {% pkg_details_link pkg.base_package %} + {% with pkg.base_package as base %}{% if base %} + {% pkg_details_link base %} {% else %} {{ pkg.pkgbase }} - {% endif %} + {% endif %}{% endwith %} {% endifequal %} -- cgit v1.2.3-54-g00ecf From fc7eb4aebf63525155bcadd366a87eed8f161568 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 28 Nov 2012 09:28:28 -0600 Subject: Add safe_mode filter to news admin; preview uses safe mode Signed-off-by: Dan McGee --- news/admin.py | 3 ++- news/views.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/news/admin.py b/news/admin.py index ad3cf517..acceb515 100644 --- a/news/admin.py +++ b/news/admin.py @@ -4,7 +4,8 @@ class NewsAdmin(admin.ModelAdmin): list_display = ('title', 'author', 'postdate', 'last_modified', 'safe_mode') - list_filter = ('postdate', 'author') + list_filter = ('postdate', 'author', 'safe_mode') search_fields = ('title', 'content') + date_hierarchy = 'postdate' admin.site.register(News, NewsAdmin) diff --git a/news/views.py b/news/views.py index efd93fdb..0e22ac34 100644 --- a/news/views.py +++ b/news/views.py @@ -76,7 +76,7 @@ def view_redirect(request, object_id): @require_POST def preview(request): data = request.POST.get('data', '') - markup = markdown.markdown(data, safe_mode=False, enable_attributes=False) + markup = markdown.markdown(data, safe_mode=True, enable_attributes=False) return HttpResponse(markup) # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From f7fe73eff01195d3b2d8cd7898e48384d331e12e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 4 Dec 2012 16:05:00 -0600 Subject: Add charset to meta tags Signed-off-by: Dan McGee --- templates/base.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/base.html b/templates/base.html index 2617bc31..2592c9b0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,7 @@ {% load url from future %}{% load static from staticfiles %} + {% block title %}Arch Linux{% endblock %} -- cgit v1.2.3-54-g00ecf From 4c699119820dfd060de6a0385e549f3397053548 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 4 Dec 2012 21:59:29 -0600 Subject: get_latest_by cleanups Fix some that referenced non-existent attributes, and add the attribute to other models. Signed-off-by: Dan McGee --- devel/models.py | 4 ++++ main/models.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/devel/models.py b/devel/models.py index 9b6f07a7..f30bba85 100644 --- a/devel/models.py +++ b/devel/models.py @@ -50,6 +50,7 @@ class UserProfile(models.Model): class Meta: db_table = 'user_profiles' + get_latest_by = 'last_modified' verbose_name = 'Additional Profile Data' verbose_name_plural = 'Additional Profile Data' @@ -80,6 +81,7 @@ class MasterKey(models.Model): class Meta: ordering = ('created',) + get_latest_by = 'created' def __unicode__(self): return u'%s, created %s' % ( @@ -94,6 +96,8 @@ class PGPSignature(models.Model): valid = models.BooleanField(default=True) class Meta: + ordering = ('signer', 'signee') + get_latest_by = 'created' verbose_name = 'PGP signature' def __unicode__(self): diff --git a/main/models.py b/main/models.py index 603d7ccc..8e705c54 100644 --- a/main/models.py +++ b/main/models.py @@ -46,7 +46,7 @@ def __unicode__(self): class Meta: db_table = 'donors' ordering = ('name',) - get_latest_by = 'when' + get_latest_by = 'created' class Arch(models.Model): -- cgit v1.2.3-54-g00ecf From 498f9b7da0cf715f4303b425edf60b1ee6b13b3f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 11 Dec 2012 09:36:50 -0600 Subject: Bump Django version in requirements Signed-off-by: Dan McGee --- requirements.txt | 2 +- requirements_prod.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 97fbaf08..1d5b068e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==1.4.2 +Django==1.4.3 Markdown==2.2.1 South==0.7.6 django-countries==1.5 diff --git a/requirements_prod.txt b/requirements_prod.txt index 04fdf0eb..c51ba01a 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -1,4 +1,4 @@ -Django==1.4.2 +Django==1.4.3 Markdown==2.2.1 South==0.7.6 django-countries==1.5 -- cgit v1.2.3-54-g00ecf From 8a8542ede6493939bd6528a72b8fd912fdf4d14b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 11 Dec 2012 10:02:26 -0600 Subject: Use multiple separate document.ready() handlers If one of them breaks, we don't want to prevent the rest of the on-load events from firing. This is currently a problem on some browsers with the versions of jQuery and tablesorter we are using. Signed-off-by: Dan McGee --- templates/devel/packages.html | 4 +++- templates/packages/differences.html | 5 +++-- templates/packages/signoffs.html | 6 ++++-- templates/todolists/view.html | 4 +++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/templates/devel/packages.html b/templates/devel/packages.html index 4e1381ab..a62ae1ab 100644 --- a/templates/devel/packages.html +++ b/templates/devel/packages.html @@ -78,7 +78,9 @@

        Filter Packages

        {% endif %} diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index fc6adca2..b6737230 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -85,9 +85,11 @@

        Filter Displayed Signoffs

        {% endblock %} -- cgit v1.2.3-54-g00ecf From e08096ee214b1fd60d093e2902c6acec9cf4ae5f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 11 Dec 2012 21:19:16 -0600 Subject: Fix FS#32018, provides links always go to [testing] packages Remove some of the smarts and do less, but be better about properly sorting the items as one might expect. Signed-off-by: Dan McGee --- packages/models.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/models.py b/packages/models.py index ede8c275..a4095f53 100644 --- a/packages/models.py +++ b/packages/models.py @@ -412,24 +412,12 @@ def get_providers(self): new_pkgs.append(package) pkgs = new_pkgs - # Logic here is to filter out packages that are in multiple repos if - # they are not requested. For example, if testing is False, only show a - # testing package if it doesn't exist in a non-testing repo. - filtered = {} - for package in pkgs: - if package.pkgname not in filtered or \ - package.repo.staging == self.pkg.repo.staging: - filtered[package.pkgname] = package - pkgs = filtered.values() - - filtered = {} - for package in pkgs: - if package.pkgname not in filtered or \ - package.repo.testing == self.pkg.repo.testing: - filtered[package.pkgname] = package - pkgs = filtered.values() - - return pkgs + # Sort providers by preference. We sort those in same staging/testing + # combination first, followed by others. We sort by a (staging, + # testing) match tuple that will be (True, True) in the best case. + key_func = lambda x: (x.repo.staging == self.pkg.repo.staging, + x.repo.testing == self.pkg.repo.testing) + return sorted(pkgs, key=key_func, reverse=True) def __unicode__(self): if self.version: -- cgit v1.2.3-54-g00ecf From aacb4978519b90e74bdca581810a3ffa0a588dcd Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 18 Dec 2012 10:24:08 -0600 Subject: Allow data in developer biographies to wrap Signed-off-by: Dan McGee --- sitestatic/archweb.css | 1 + 1 file changed, 1 insertion(+) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 52aec59f..50f0f488 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -952,6 +952,7 @@ table td.country { .arch-bio-entry table.bio td { width: 100%; padding-bottom: 0.25em; + white-space: normal; } /* dev: login/out */ -- cgit v1.2.3-54-g00ecf From 724422850020fd556e77026e54bfb56aa595eddc Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 27 Dec 2012 16:28:43 -0600 Subject: Tablesorter JS upgrade Signed-off-by: Dan McGee --- main/templatetags/cdn.py | 2 +- sitestatic/archweb.css | 60 +- sitestatic/jquery.tablesorter-2.4.5.js | 1333 --------------------------- sitestatic/jquery.tablesorter-2.4.5.min.js | 6 - sitestatic/jquery.tablesorter-2.7.js | 1374 ++++++++++++++++++++++++++++ sitestatic/jquery.tablesorter-2.7.min.js | 5 + 6 files changed, 1404 insertions(+), 1376 deletions(-) delete mode 100644 sitestatic/jquery.tablesorter-2.4.5.js delete mode 100644 sitestatic/jquery.tablesorter-2.4.5.min.js create mode 100644 sitestatic/jquery.tablesorter-2.7.js create mode 100644 sitestatic/jquery.tablesorter-2.7.min.js diff --git a/main/templatetags/cdn.py b/main/templatetags/cdn.py index 771b4426..fc63fdd8 100644 --- a/main/templatetags/cdn.py +++ b/main/templatetags/cdn.py @@ -20,7 +20,7 @@ def jquery(): @register.simple_tag def jquery_tablesorter(): - version = '2.4.5' + version = '2.7' filename = 'jquery.tablesorter-%s.min.js' % version link = staticfiles_storage.url(filename) return '' % link diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 50f0f488..45029f10 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -292,23 +292,6 @@ table.pretty2 { border: 1px solid #bbb; } - /* additional styles for JS sorting */ - table.pretty2 th.tablesorter-header { - padding-right: 20px; - background-image: url(nosort.gif); - background-repeat: no-repeat; - background-position: center right; - cursor: pointer; - } - - table.pretty2 th.tablesorter-headerSortDown { - background-image: url(desc.gif); - } - - table.pretty2 th.tablesorter-headerSortUp { - background-image: url(asc.gif); - } - table.pretty2 td { padding: 0.35em; border: 1px dotted #bbb; @@ -417,6 +400,30 @@ ul.errorlist { color: red; } +/* JS sorting via tablesorter */ +table th.tablesorter-header { + padding-right: 20px; + background-image: url(data:image/gif;base64,R0lGODlhFQAJAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAkAAAIXjI+AywnaYnhUMoqt3gZXPmVg94yJVQAAOw==); + background-repeat: no-repeat; + background-position: center right; + cursor: pointer; +} + +table thead th.tablesorter-headerAsc { + background-color: #e4eeff; + background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7); +} + +table thead th.tablesorter-headerDesc { + background-color: #e4eeff; + background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7); +} + +table thead th.sorter-false { + background-image: url(); + cursor: default; +} + /** * PAGE SPECIFIC STYLES */ @@ -666,25 +673,6 @@ table.results { background-color:#fff; } - /* additional styles for JS sorting */ - table.results th.tablesorter-header { - padding-right: 20px; - background-image: url(nosort.gif); - background-repeat: no-repeat; - background-position: center right; - cursor: pointer; - } - - table.results th.tablesorter-headerSortDown { - background-color: #e4eeff; - background-image: url(desc.gif); - } - - table.results th.tablesorter-headerSortUp { - background-color: #e4eeff; - background-image: url(asc.gif); - } - table.results td { padding: .3em 1em .3em 3px; } diff --git a/sitestatic/jquery.tablesorter-2.4.5.js b/sitestatic/jquery.tablesorter-2.4.5.js deleted file mode 100644 index 723cb91e..00000000 --- a/sitestatic/jquery.tablesorter-2.4.5.js +++ /dev/null @@ -1,1333 +0,0 @@ -/*! -* TableSorter 2.4.5 - Client-side table sorting with ease! -* @requires jQuery v1.2.6+ -* -* Copyright (c) 2007 Christian Bach -* Examples and docs at: http://tablesorter.com -* Dual licensed under the MIT and GPL licenses: -* http://www.opensource.org/licenses/mit-license.php -* http://www.gnu.org/licenses/gpl.html -* -* @type jQuery -* @name tablesorter -* @cat Plugins/Tablesorter -* @author Christian Bach/christian.bach@polyester.se -* @contributor Rob Garrison/https://github.com/Mottie/tablesorter -*/ -/*jshint evil:true, browser:true, jquery:true, unused:false */ -/*global console:false, alert:false */ -!(function($) { - "use strict"; - $.extend({ - tablesorter: new function() { - - var ts = this; - - ts.version = "2.4.5"; - - ts.parsers = []; - ts.widgets = []; - ts.defaults = { - - // appearance - theme : 'default', // adds tablesorter-{theme} to the table for styling - widthFixed : false, // adds colgroup to fix widths of columns - showProcessing : false, // show an indeterminate timer icon in the header when the table is sorted or filtered. - - // functionality - cancelSelection : true, // prevent text selection in the header - dateFormat : 'mmddyyyy', // other options: "ddmmyyy" or "yyyymmdd" - sortMultiSortKey : 'shiftKey', // key used to select additional columns - usNumberFormat : true, // false for German "1.234.567,89" or French "1 234 567,89" - delayInit : false, // if false, the parsed table contents will not update until the first sort - - // sort options - headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc. - ignoreCase : true, // ignore case while sorting - sortForce : null, // column(s) first sorted; always applied - sortList : [], // Initial sort order; applied initially; updated when manually sorted - sortAppend : null, // column(s) sorted last; always applied - - sortInitialOrder : 'asc', // sort direction on first click - sortLocaleCompare: false, // replace equivalent character (accented characters) - sortReset : false, // third click on the header will reset column to default - unsorted - sortRestart : false, // restart sort to "sortInitialOrder" when clicking on previously unsorted columns - - emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero - stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero - textExtraction : 'simple', // text extraction method/function - function(node, table, cellIndex){} - textSorter : null, // use custom text sorter - function(a,b){ return a.sort(b); } // basic sort - - // widget options - widgets: [], // method to add widgets, e.g. widgets: ['zebra'] - widgetOptions : { - zebra : [ 'even', 'odd' ] // zebra widget alternating row class names - }, - initWidgets : true, // apply widgets on tablesorter initialization - - // callbacks - initialized : null, // function(table){}, - onRenderHeader : null, // function(index){}, - - // css class names - tableClass : 'tablesorter', - cssAsc : 'tablesorter-headerSortUp', - cssChildRow : 'tablesorter-childRow', // previously "expand-child" - cssDesc : 'tablesorter-headerSortDown', - cssHeader : 'tablesorter-header', - cssHeaderRow : 'tablesorter-headerRow', - cssIcon : 'tablesorter-icon', // if this class exists, a will be added to the header automatically - cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name - cssProcessing : 'tablesorter-processing', // processing icon applied to header during sort/filter - - // selectors - selectorHeaders : '> thead th, > thead td', - selectorSort : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort - selectorRemove : '.remove-me', - - // advanced - debug : false, - - // Internal variables - headerList: [], - empties: {}, - strings: {}, - parsers: [] - - // deprecated; but retained for backwards compatibility - // widgetZebra: { css: ["even", "odd"] } - - }; - - /* debuging utils */ - function log(s) { - if (typeof console !== "undefined" && typeof console.log !== "undefined") { - console.log(s); - } else { - alert(s); - } - } - - function benchmark(s, d) { - log(s + " (" + (new Date().getTime() - d.getTime()) + "ms)"); - } - - ts.benchmark = benchmark; - - function getElementText(table, node, cellIndex) { - if (!node) { return ""; } - var c = table.config, - t = c.textExtraction, text = ""; - if (t === "simple") { - if (c.supportsTextContent) { - text = node.textContent; // newer browsers support this - } else { - text = $(node).text(); - } - } else { - if (typeof(t) === "function") { - text = t(node, table, cellIndex); - } else if (typeof(t) === "object" && t.hasOwnProperty(cellIndex)) { - text = t[cellIndex](node, table, cellIndex); - } else { - text = c.supportsTextContent ? node.textContent : $(node).text(); - } - } - return $.trim(text); - } - - function detectParserForColumn(table, rows, rowIndex, cellIndex) { - var i, l = ts.parsers.length, - node = false, - nodeValue = '', - keepLooking = true; - while (nodeValue === '' && keepLooking) { - rowIndex++; - if (rows[rowIndex]) { - node = rows[rowIndex].cells[cellIndex]; - nodeValue = getElementText(table, node, cellIndex); - if (table.config.debug) { - log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': ' + nodeValue); - } - } else { - keepLooking = false; - } - } - for (i = 1; i < l; i++) { - if (ts.parsers[i].is(nodeValue, table, node)) { - return ts.parsers[i]; - } - } - // 0 is always the generic parser (text) - return ts.parsers[0]; - } - - function buildParserCache(table, $headers) { - var c = table.config, - tb = $(table.tBodies).filter(':not(.' + c.cssInfoBlock + ')'), - rows, list, l, i, h, ch, p, parsersDebug = ""; - if ( tb.length === 0) { return; } // In the case of empty tables - rows = tb[0].rows; - if (rows[0]) { - list = []; - l = rows[0].cells.length; - for (i = 0; i < l; i++) { - // tons of thanks to AnthonyM1229 for working out the following selector (issue #74) to make this work in IE8! - h = $headers.filter(':not([colspan])[data-column="' + i + '"]:last,[colspan="1"][data-column="' + i + '"]:last'); - ch = c.headers[i]; - // get column parser - p = ts.getParserById( ts.getData(h, ch, 'sorter') ); - // empty cells behaviour - keeping emptyToBottom for backwards compatibility - c.empties[i] = ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' ); - // text strings behaviour in numerical sorts - c.strings[i] = ts.getData(h, ch, 'string') || c.stringTo || 'max'; - if (!p) { - p = detectParserForColumn(table, rows, -1, i); - } - if (c.debug) { - parsersDebug += "column:" + i + "; parser:" + p.id + "; string:" + c.strings[i] + '; empty: ' + c.empties[i] + "\n"; - } - list.push(p); - } - } - if (c.debug) { - log(parsersDebug); - } - return list; - } - - /* utils */ - function buildCache(table) { - var b = table.tBodies, - tc = table.config, - totalRows, - totalCells, - parsers = tc.parsers, - t, i, j, k, c, cols, cacheTime; - tc.cache = {}; - if (tc.debug) { - cacheTime = new Date(); - } - // processing icon - if (tc.showProcessing) { - ts.isProcessing(table, true); - } - for (k = 0; k < b.length; k++) { - tc.cache[k] = { row: [], normalized: [] }; - // ignore tbodies with class name from css.cssInfoBlock - if (!$(b[k]).hasClass(tc.cssInfoBlock)) { - totalRows = (b[k] && b[k].rows.length) || 0; - totalCells = (b[k].rows[0] && b[k].rows[0].cells.length) || 0; - for (i = 0; i < totalRows; ++i) { - /** Add the table data to main data array */ - c = $(b[k].rows[i]); - cols = []; - // if this is a child row, add it to the last row's children and continue to the next row - if (c.hasClass(tc.cssChildRow)) { - tc.cache[k].row[tc.cache[k].row.length - 1] = tc.cache[k].row[tc.cache[k].row.length - 1].add(c); - // go to the next for loop - continue; - } - tc.cache[k].row.push(c); - for (j = 0; j < totalCells; ++j) { - t = getElementText(table, c[0].cells[j], j); - // allow parsing if the string is empty, previously parsing would change it to zero, - // in case the parser needs to extract data from the table cell attributes - cols.push( parsers[j].format(t, table, c[0].cells[j], j) ); - } - cols.push(tc.cache[k].normalized.length); // add position for rowCache - tc.cache[k].normalized.push(cols); - } - } - } - if (tc.showProcessing) { - ts.isProcessing(table); // remove processing icon - } - if (tc.debug) { - benchmark("Building cache for " + totalRows + " rows", cacheTime); - } - } - - // init flag (true) used by pager plugin to prevent widget application - function appendToTable(table, init) { - var c = table.config, - b = table.tBodies, - rows = [], - c2 = c.cache, - r, n, totalRows, checkCell, $bk, $tb, - i, j, k, l, pos, appendTime; - if (c.debug) { - appendTime = new Date(); - } - for (k = 0; k < b.length; k++) { - $bk = $(b[k]); - if (!$bk.hasClass(c.cssInfoBlock)) { - // get tbody - $tb = ts.processTbody(table, $bk, true); - r = c2[k].row; - n = c2[k].normalized; - totalRows = n.length; - checkCell = totalRows ? (n[0].length - 1) : 0; - for (i = 0; i < totalRows; i++) { - pos = n[i][checkCell]; - rows.push(r[pos]); - // removeRows used by the pager plugin - if (!c.appender || !c.removeRows) { - l = r[pos].length; - for (j = 0; j < l; j++) { - $tb.append(r[pos][j]); - } - } - } - // restore tbody - ts.processTbody(table, $tb, false); - } - } - if (c.appender) { - c.appender(table, rows); - } - if (c.debug) { - benchmark("Rebuilt table", appendTime); - } - // apply table widgets - if (!init) { ts.applyWidget(table); } - // trigger sortend - $(table).trigger("sortEnd", table); - } - - // computeTableHeaderCellIndexes from: - // http://www.javascripttoolbox.com/lib/table/examples.php - // http://www.javascripttoolbox.com/temp/table_cellindex.html - function computeThIndexes(t) { - var matrix = [], - lookup = {}, - trs = $(t).find('thead:eq(0) tr, tfoot tr'), - i, j, k, l, c, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol, matrixrow; - for (i = 0; i < trs.length; i++) { - cells = trs[i].cells; - for (j = 0; j < cells.length; j++) { - c = cells[j]; - rowIndex = c.parentNode.rowIndex; - cellId = rowIndex + "-" + c.cellIndex; - rowSpan = c.rowSpan || 1; - colSpan = c.colSpan || 1; - if (typeof(matrix[rowIndex]) === "undefined") { - matrix[rowIndex] = []; - } - // Find first available column in the first row - for (k = 0; k < matrix[rowIndex].length + 1; k++) { - if (typeof(matrix[rowIndex][k]) === "undefined") { - firstAvailCol = k; - break; - } - } - lookup[cellId] = firstAvailCol; - // add data-column - $(c).attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex - for (k = rowIndex; k < rowIndex + rowSpan; k++) { - if (typeof(matrix[k]) === "undefined") { - matrix[k] = []; - } - matrixrow = matrix[k]; - for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) { - matrixrow[l] = "x"; - } - } - } - } - return lookup; - } - - function formatSortingOrder(v) { - // look for "d" in "desc" order; return true - return (/^d/i.test(v) || v === 1); - } - - function buildHeaders(table) { - var header_index = computeThIndexes(table), ch, $t, - t, lock, time, $tableHeaders, c = table.config; - c.headerList = []; - if (c.debug) { - time = new Date(); - } - $tableHeaders = $(table).find(c.selectorHeaders).each(function(index) { - $t = $(this); - ch = c.headers[index]; - t = c.cssIcon ? '' : ''; // add icon if cssIcon option exists - this.innerHTML = '
        ' + this.innerHTML + t + '
        '; // faster than wrapInner - if (c.onRenderHeader) { c.onRenderHeader.apply($t, [index]); } - this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; - this.order = formatSortingOrder( ts.getData($t, ch, 'sortInitialOrder') || c.sortInitialOrder ) ? [1,0,2] : [0,1,2]; - this.count = -1; // set to -1 because clicking on the header automatically adds one - if (ts.getData($t, ch, 'sorter') === 'false') { - this.sortDisabled = true; - $t.addClass('sorter-false'); - } else { - $t.removeClass('sorter-false'); - } - this.lockedOrder = false; - lock = ts.getData($t, ch, 'lockedOrder') || false; - if (typeof(lock) !== 'undefined' && lock !== false) { - this.order = this.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0]; - } - $t.addClass( this.sortDisabled ? 'sorter-false' : c.cssHeader ); - // add cell to headerList - c.headerList[index] = this; - // add to parent in case there are multiple rows - $t.parent().addClass(c.cssHeaderRow); - }); - if (table.config.debug) { - benchmark("Built headers:", time); - log($tableHeaders); - } - return $tableHeaders; - } - - function setHeadersCss(table, $headers) { - var f, i, j, l, - c = table.config, - list = c.sortList, - css = [c.cssDesc, c.cssAsc], - // find the footer - $t = $(table).find('tfoot tr').children().removeClass(css.join(' ')); - // remove all header information - $headers.removeClass(css.join(' ')); - l = list.length; - for (i = 0; i < l; i++) { - // direction = 2 means reset! - if (list[i][1] !== 2) { - // multicolumn sorting updating - choose the :last in case there are nested columns - f = $headers.not('.sorter-false').filter('[data-column="' + list[i][0] + '"]' + (l === 1 ? ':last' : '') ); - if (f.length) { - for (j = 0; j < f.length; j++) { - if (!f[j].sortDisabled) { - f.eq(j).addClass(css[list[i][1]]); - // add sorted class to footer, if it exists - if ($t.length) { - $t.filter('[data-column="' + list[i][0] + '"]').eq(j).addClass(css[list[i][1]]); - } - } - } - } - } - } - } - - function fixColumnWidth(table) { - if (table.config.widthFixed && $(table).find('colgroup').length === 0) { - var colgroup = $(''), - overallWidth = $(table).width(); - $("tr:first td", table.tBodies[0]).each(function() { - colgroup.append($('').css('width', parseInt(($(this).width()/overallWidth)*1000, 10)/10 + '%')); - }); - $(table).prepend(colgroup); - } - } - - function updateHeaderSortCount(table, list) { - var s, o, c = table.config, - l = c.headerList.length, - sl = list || c.sortList; - c.sortList = []; - $.each(sl, function(i,v){ - // ensure all sortList values are numeric - fixes #127 - s = [ parseInt(v[0], 10), parseInt(v[1], 10) ]; - // make sure header exists - o = c.headerList[s[0]]; - if (o) { // prevents error if sorton array is wrong - c.sortList.push(s); - o.count = s[1] % (c.sortReset ? 3 : 2); - } - }); - } - - function getCachedSortType(parsers, i) { - return (parsers && parsers[i]) ? parsers[i].type || '' : ''; - } - - // sort multiple columns - function multisort(table) { - var dynamicExp, sortWrapper, col, mx = 0, dir = 0, tc = table.config, - sortList = tc.sortList, l = sortList.length, bl = table.tBodies.length, - sortTime, i, j, k, c, cache, lc, s, e, order, orgOrderCol; - if (tc.debug) { sortTime = new Date(); } - for (k = 0; k < bl; k++) { - dynamicExp = "sortWrapper = function(a,b) {"; - cache = tc.cache[k]; - lc = cache.normalized.length; - for (i = 0; i < l; i++) { - c = sortList[i][0]; - order = sortList[i][1]; - // fallback to natural sort since it is more robust - s = /n/i.test(getCachedSortType(tc.parsers, c)) ? "Numeric" : "Text"; - s += order === 0 ? "" : "Desc"; - e = "e" + i; - // get max column value (ignore sign) - if (/Numeric/.test(s) && tc.strings[c]) { - for (j = 0; j < lc; j++) { - col = Math.abs(parseFloat(cache.normalized[j][c])); - mx = Math.max( mx, isNaN(col) ? 0 : col ); - } - // sort strings in numerical columns - if (typeof(tc.string[tc.strings[c]]) === 'boolean') { - dir = (order === 0 ? 1 : -1) * (tc.string[tc.strings[c]] ? -1 : 1); - } else { - dir = (tc.strings[c]) ? tc.string[tc.strings[c]] || 0 : 0; - } - } - dynamicExp += "var " + e + " = $.tablesorter.sort" + s + "(table,a[" + c + "],b[" + c + "]," + c + "," + mx + "," + dir + "); "; - dynamicExp += "if (" + e + ") { return " + e + "; } "; - dynamicExp += "else { "; - } - // if value is the same keep orignal order - orgOrderCol = (cache.normalized && cache.normalized[0]) ? cache.normalized[0].length - 1 : 0; - dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];"; - for (i=0; i < l; i++) { - dynamicExp += "}; "; - } - dynamicExp += "return 0; "; - dynamicExp += "}; "; - cache.normalized.sort(eval(dynamicExp)); // sort using eval expression - } - if (tc.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time", sortTime); } - } - - function resortComplete($table, callback){ - var t = $table[0]; - $table.trigger('updateComplete'); - if (typeof callback === "function") { - callback(t); - } - } - - function checkResort($table, flag, callback) { - if (flag !== false) { - $table.trigger("sorton", [$table[0].config.sortList, function(){ - resortComplete($table, callback); - }]); - } else { - resortComplete($table, callback); - } - } - - /* public methods */ - ts.construct = function(settings) { - return this.each(function() { - // if no thead or tbody, or tablesorter is already present, quit - if (!this.tHead || this.tBodies.length === 0 || this.hasInitialized === true) { return; } - // declare - var $headers, $cell, $this = $(this), - c, i, j, k = '', a, s, o, downTime, - m = $.metadata; - // initialization flag - this.hasInitialized = false; - // new blank config object - this.config = {}; - // merge and extend - c = $.extend(true, this.config, ts.defaults, settings); - // save the settings where they read - $.data(this, "tablesorter", c); - if (c.debug) { $.data( this, 'startoveralltimer', new Date()); } - // constants - c.supportsTextContent = $('x')[0].textContent === 'x'; - c.supportsDataObject = parseFloat($.fn.jquery) >= 1.4; - // digit sort text location; keeping max+/- for backwards compatibility - c.string = { 'max': 1, 'min': -1, 'max+': 1, 'max-': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false }; - // add table theme class only if there isn't already one there - if (!/tablesorter\-/.test($this.attr('class'))) { - k = (c.theme !== '' ? ' tablesorter-' + c.theme : ''); - } - $this.addClass(c.tableClass + k); - // build headers - $headers = buildHeaders(this); - // try to auto detect column type, and store in tables config - c.parsers = buildParserCache(this, $headers); - // build the cache for the tbody cells - // delayInit will delay building the cache until the user starts a sort - if (!c.delayInit) { buildCache(this); } - // apply event handling to headers - // this is to big, perhaps break it out? - $headers - // http://stackoverflow.com/questions/5312849/jquery-find-self - .find('*').andSelf().filter(c.selectorSort) - .unbind('mousedown.tablesorter mouseup.tablesorter') - .bind('mousedown.tablesorter mouseup.tablesorter', function(e, external) { - // jQuery v1.2.6 doesn't have closest() - var $cell = this.tagName.match('TH|TD') ? $(this) : $(this).parents('th, td').filter(':last'), cell = $cell[0]; - // only recognize left clicks - if ((e.which || e.button) !== 1) { return false; } - // set timer on mousedown - if (e.type === 'mousedown') { - downTime = new Date().getTime(); - return e.target.tagName === "INPUT" ? '' : !c.cancelSelection; - } - // ignore long clicks (prevents resizable widget from initializing a sort) - if (external !== true && (new Date().getTime() - downTime > 250)) { return false; } - if (c.delayInit && !c.cache) { buildCache($this[0]); } - if (!cell.sortDisabled) { - // Only call sortStart if sorting is enabled - $this.trigger("sortStart", $this[0]); - // store exp, for speed - // $cell = $(this); - k = !e[c.sortMultiSortKey]; - // get current column sort order - cell.count = (cell.count + 1) % (c.sortReset ? 3 : 2); - // reset all sorts on non-current column - issue #30 - if (c.sortRestart) { - i = cell; - $headers.each(function() { - // only reset counts on columns that weren't just clicked on and if not included in a multisort - if (this !== i && (k || !$(this).is('.' + c.cssDesc + ',.' + c.cssAsc))) { - this.count = -1; - } - }); - } - // get current column index - i = cell.column; - // user only wants to sort on one column - if (k) { - // flush the sort list - c.sortList = []; - if (c.sortForce !== null) { - a = c.sortForce; - for (j = 0; j < a.length; j++) { - if (a[j][0] !== i) { - c.sortList.push(a[j]); - } - } - } - // add column to sort list - o = cell.order[cell.count]; - if (o < 2) { - c.sortList.push([i, o]); - // add other columns if header spans across multiple - if (cell.colSpan > 1) { - for (j = 1; j < cell.colSpan; j++) { - c.sortList.push([i + j, o]); - } - } - } - // multi column sorting - } else { - // get rid of the sortAppend before adding more - fixes issue #115 - if (c.sortAppend && c.sortList.length > 1) { - if (ts.isValueInArray(c.sortAppend[0][0], c.sortList)) { - c.sortList.pop(); - } - } - // the user has clicked on an already sorted column - if (ts.isValueInArray(i, c.sortList)) { - // reverse the sorting direction for all tables - for (j = 0; j < c.sortList.length; j++) { - s = c.sortList[j]; - o = c.headerList[s[0]]; - if (s[0] === i) { - s[1] = o.order[o.count]; - if (s[1] === 2) { - c.sortList.splice(j,1); - o.count = -1; - } - } - } - } else { - // add column to sort list array - o = cell.order[cell.count]; - if (o < 2) { - c.sortList.push([i, o]); - // add other columns if header spans across multiple - if (cell.colSpan > 1) { - for (j = 1; j < cell.colSpan; j++) { - c.sortList.push([i + j, o]); - } - } - } - } - } - if (c.sortAppend !== null) { - a = c.sortAppend; - for (j = 0; j < a.length; j++) { - if (a[j][0] !== i) { - c.sortList.push(a[j]); - } - } - } - // sortBegin event triggered immediately before the sort - $this.trigger("sortBegin", $this[0]); - // setTimeout needed so the processing icon shows up - setTimeout(function(){ - // set css for headers - setHeadersCss($this[0], $headers); - multisort($this[0]); - appendToTable($this[0]); - }, 1); - } - }); - if (c.cancelSelection) { - // cancel selection - $headers.each(function() { - this.onselectstart = function() { - return false; - }; - }); - } - // apply easy methods that trigger binded events - $this - .unbind('update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave') - .bind("update", function(e, resort, callback) { - // remove rows/elements before update - $(c.selectorRemove, this).remove(); - // rebuild parsers - c.parsers = buildParserCache(this, $headers); - // rebuild the cache map - buildCache(this); - checkResort($this, resort, callback); - }) - .bind("updateCell", function(e, cell, resort, callback) { - // get position from the dom - var l, row, icell, - t = this, $tb = $(this).find('tbody'), - // update cache - format: function(s, table, cell, cellIndex) - // no closest in jQuery v1.2.6 - tbdy = $tb.index( $(cell).closest('tbody') ),$row = $(cell).closest('tr'); - tbdy = $tb.index( $(cell).parents('tbody').filter(':last') ), - $row = $(cell).parents('tr').filter(':last'); - // tbody may not exist if update is initialized while tbody is removed for processing - if ($tb.length && tbdy >= 0) { - row = $tb.eq(tbdy).find('tr').index( $row ); - icell = cell.cellIndex; - l = t.config.cache[tbdy].normalized[row].length - 1; - t.config.cache[tbdy].row[t.config.cache[tbdy].normalized[row][l]] = $row; - t.config.cache[tbdy].normalized[row][icell] = c.parsers[icell].format( getElementText(t, cell, icell), t, cell, icell ); - checkResort($this, resort, callback); - } - }) - .bind("addRows", function(e, $row, resort, callback) { - var i, rows = $row.filter('tr').length, - dat = [], l = $row[0].cells.length, t = this, - tbdy = $(this).find('tbody').index( $row.closest('tbody') ); - // add each row - for (i = 0; i < rows; i++) { - // add each cell - for (j = 0; j < l; j++) { - dat[j] = c.parsers[j].format( getElementText(t, $row[i].cells[j], j), t, $row[i].cells[j], j ); - } - // add the row index to the end - dat.push(c.cache[tbdy].row.length); - // update cache - c.cache[tbdy].row.push([$row[i]]); - c.cache[tbdy].normalized.push(dat); - dat = []; - } - // resort using current settings - checkResort($this, resort, callback); - }) - .bind("sorton", function(e, list, callback, init) { - $(this).trigger("sortStart", this); - // update header count index - updateHeaderSortCount(this, list); - // set css for headers - setHeadersCss(this, $headers); - // sort the table and append it to the dom - multisort(this); - appendToTable(this, init); - if (typeof callback === "function") { - callback(this); - } - }) - .bind("appendCache", function(e, callback, init) { - appendToTable(this, init); - if (typeof callback === "function") { - callback(this); - } - }) - .bind("applyWidgetId", function(e, id) { - ts.getWidgetById(id).format(this, c, c.widgetOptions); - }) - .bind("applyWidgets", function(e, init) { - // apply widgets - ts.applyWidget(this, init); - }) - .bind("refreshWidgets", function(e, all, dontapply){ - ts.refreshWidgets(this, all, dontapply); - }) - .bind("destroy", function(e, c, cb){ - ts.destroy(this, c, cb); - }); - - // get sort list from jQuery data or metadata - // in jQuery < 1.4, an error occurs when calling $this.data() - if (c.supportsDataObject && typeof $this.data().sortlist !== 'undefined') { - c.sortList = $this.data().sortlist; - } else if (m && ($this.metadata() && $this.metadata().sortlist)) { - c.sortList = $this.metadata().sortlist; - } - // apply widget init code - ts.applyWidget(this, true); - // if user has supplied a sort list to constructor - if (c.sortList.length > 0) { - $this.trigger("sorton", [c.sortList, {}, !c.initWidgets]); - } else if (c.initWidgets) { - // apply widget format - ts.applyWidget(this); - } - - // fixate columns if the users supplies the fixedWidth option - // do this after theme has been applied - fixColumnWidth(this); - - // show processesing icon - if (c.showProcessing) { - $this - .unbind('sortBegin sortEnd') - .bind('sortBegin sortEnd', function(e) { - ts.isProcessing($this[0], e.type === 'sortBegin'); - }); - } - - // initialized - this.hasInitialized = true; - if (c.debug) { - ts.benchmark("Overall initialization time", $.data( this, 'startoveralltimer')); - } - $this.trigger('tablesorter-initialized', this); - if (typeof c.initialized === 'function') { c.initialized(this); } - }); - }; - - // *** Process table *** - // add processing indicator - ts.isProcessing = function(table, toggle, $ths) { - var c = table.config, - // default to all headers - $h = $ths || $(table).find('.' + c.cssHeader); - if (toggle) { - if (c.sortList.length > 0) { - // get headers from the sortList - $h = $h.filter(function(){ - // get data-column from attr to keep compatibility with jQuery 1.2.6 - return this.sortDisabled ? false : ts.isValueInArray( parseFloat($(this).attr('data-column')), c.sortList); - }); - } - $h.addClass(c.cssProcessing); - } else { - $h.removeClass(c.cssProcessing); - } - }; - - // detach tbody but save the position - // don't use tbody because there are portions that look for a tbody index (updateCell) - ts.processTbody = function(table, $tb, getIt){ - var t, holdr; - if (getIt) { - $tb.before(''); - holdr = ($.fn.detach) ? $tb.detach() : $tb.remove(); - return holdr; - } - holdr = $(table).find('span.tablesorter-savemyplace'); - $tb.insertAfter( holdr ); - holdr.remove(); - }; - - ts.clearTableBody = function(table) { - $(table.tBodies).filter(':not(.' + table.config.cssInfoBlock + ')').empty(); - }; - - ts.destroy = function(table, removeClasses, callback){ - var $t = $(table), c = table.config, - $h = $t.find('thead:first'); - // clear flag in case the plugin is initialized again - table.hasInitialized = false; - // remove widget added rows - $h.find('tr:not(.' + c.cssHeaderRow + ')').remove(); - // remove resizer widget stuff - $h.find('.tablesorter-resizer').remove(); - // remove all widgets - ts.refreshWidgets(table, true, true); - // disable tablesorter - $t - .removeData('tablesorter') - .unbind('update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave') - .find('.' + c.cssHeader) - .unbind('click mousedown mousemove mouseup') - .removeClass(c.cssHeader + ' ' + c.cssAsc + ' ' + c.cssDesc) - .find('.tablesorter-header-inner').each(function(){ - if (c.cssIcon !== '') { $(this).find('.' + c.cssIcon).remove(); } - $(this).replaceWith( $(this).contents() ); - }); - if (removeClasses !== false) { - $t.removeClass(c.tableClass); - } - if (typeof callback === 'function') { - callback(table); - } - }; - - // *** sort functions *** - // regex used in natural sort - ts.regex = [ - /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi, // chunk/tokenize numbers & letters - /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/, //date - /^0x[0-9a-f]+$/i // hex - ]; - - // Natural sort - https://github.com/overset/javascript-natural-sort - ts.sortText = function(table, a, b, col) { - if (a === b) { return 0; } - var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ], - r = ts.regex, xN, xD, yN, yD, xF, yF, i, mx; - if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } - if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } - if (typeof c.textSorter === 'function') { return c.textSorter(a, b, table, col); } - // chunk/tokenize - xN = a.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); - yN = b.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); - // numeric, hex or date detection - xD = parseInt(a.match(r[2]),16) || (xN.length !== 1 && a.match(r[1]) && Date.parse(a)); - yD = parseInt(b.match(r[2]),16) || (xD && b.match(r[1]) && Date.parse(b)) || null; - // first try and sort Hex codes or Dates - if (yD) { - if ( xD < yD ) { return -1; } - if ( xD > yD ) { return 1; } - } - mx = Math.max(xN.length, yN.length); - // natural sorting through split numeric strings and default strings - for (i = 0; i < mx; i++) { - // find floats not starting with '0', string or 0 if not defined - xF = isNaN(xN[i]) ? xN[i] || 0 : parseFloat(xN[i]) || 0; - yF = isNaN(yN[i]) ? yN[i] || 0 : parseFloat(yN[i]) || 0; - // handle numeric vs string comparison - number < string - (Kyle Adams) - if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; } - // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' - if (typeof xF !== typeof yF) { - xF += ''; - yF += ''; - } - if (xF < yF) { return -1; } - if (xF > yF) { return 1; } - } - return 0; - }; - - ts.sortTextDesc = function(table, a, b, col) { - if (a === b) { return 0; } - var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; - if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } - if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } - if (typeof c.textSorter === 'function') { return c.textSorter(b, a, table, col); } - return ts.sortText(table, b, a); - }; - - // return text string value by adding up ascii value - // so the text is somewhat sorted when using a digital sort - // this is NOT an alphanumeric sort - ts.getTextValue = function(a, mx, d) { - if (mx) { - // make sure the text value is greater than the max numerical value (mx) - var i, l = a.length, n = mx + d; - for (i = 0; i < l; i++) { - n += a.charCodeAt(i); - } - return d * n; - } - return 0; - }; - - ts.sortNumeric = function(table, a, b, col, mx, d) { - if (a === b) { return 0; } - var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; - if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } - if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } - if (isNaN(a)) { a = ts.getTextValue(a, mx, d); } - if (isNaN(b)) { b = ts.getTextValue(b, mx, d); } - return a - b; - }; - - ts.sortNumericDesc = function(table, a, b, col, mx, d) { - if (a === b) { return 0; } - var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; - if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } - if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } - if (isNaN(a)) { a = ts.getTextValue(a, mx, d); } - if (isNaN(b)) { b = ts.getTextValue(b, mx, d); } - return b - a; - }; - - // used when replacing accented characters during sorting - ts.characterEquivalents = { - "a" : "\u00e1\u00e0\u00e2\u00e3\u00e4", // áàâãä - "A" : "\u00c1\u00c0\u00c2\u00c3\u00c4", // ÁÀÂÃÄ - "c" : "\u00e7", // ç - "C" : "\u00c7", // Ç - "e" : "\u00e9\u00e8\u00ea\u00eb", // éèêë - "E" : "\u00c9\u00c8\u00ca\u00cb", // ÉÈÊË - "i" : "\u00ed\u00ec\u0130\u00ee\u00ef", // íìİîï - "I" : "\u00cd\u00cc\u0130\u00ce\u00cf", // ÍÌİÎÏ - "o" : "\u00f3\u00f2\u00f4\u00f5\u00f6", // óòôõö - "O" : "\u00d3\u00d2\u00d4\u00d5\u00d6", // ÓÒÔÕÖ - "S" : "\u00df", // ß - "u" : "\u00fa\u00f9\u00fb\u00fc", // úùûü - "U" : "\u00da\u00d9\u00db\u00dc" // ÚÙÛÜ - }; - ts.replaceAccents = function(s) { - var a, acc = '[', eq = ts.characterEquivalents; - if (!ts.characterRegex) { - ts.characterRegexArray = {}; - for (a in eq) { - if (typeof a === 'string') { - acc += eq[a]; - ts.characterRegexArray[a] = new RegExp('[' + eq[a] + ']', 'g'); - } - } - ts.characterRegex = new RegExp(acc + ']'); - } - if (ts.characterRegex.test(s)) { - for (a in eq) { - if (typeof a === 'string') { - s = s.replace( ts.characterRegexArray[a], a ); - } - } - } - return s; - }; - - // *** utilities *** - ts.isValueInArray = function(v, a) { - var i, l = a.length; - for (i = 0; i < l; i++) { - if (a[i][0] === v) { - return true; - } - } - return false; - }; - - ts.addParser = function(parser) { - var i, l = ts.parsers.length, a = true; - for (i = 0; i < l; i++) { - if (ts.parsers[i].id.toLowerCase() === parser.id.toLowerCase()) { - a = false; - } - } - if (a) { - ts.parsers.push(parser); - } - }; - - ts.getParserById = function(name) { - var i, l = ts.parsers.length; - for (i = 0; i < l; i++) { - if (ts.parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) { - return ts.parsers[i]; - } - } - return false; - }; - - ts.addWidget = function(widget) { - ts.widgets.push(widget); - }; - - ts.getWidgetById = function(name) { - var i, w, l = ts.widgets.length; - for (i = 0; i < l; i++) { - w = ts.widgets[i]; - if (w && w.hasOwnProperty('id') && w.id.toLowerCase() === name.toLowerCase()) { - return w; - } - } - }; - - ts.applyWidget = function(table, init) { - var c = table.config, - wo = c.widgetOptions, - ws = c.widgets.sort().reverse(), // ensure that widgets are always applied in a certain order - time, i, w, l = ws.length; - // make zebra last - i = $.inArray('zebra', c.widgets); - if (i >= 0) { - c.widgets.splice(i,1); - c.widgets.push('zebra'); - } - if (c.debug) { - time = new Date(); - } - // add selected widgets - for (i = 0; i < l; i++) { - w = ts.getWidgetById(ws[i]); - if ( w ) { - if (init === true && w.hasOwnProperty('init')) { - w.init(table, w, c, wo); - } else if (!init && w.hasOwnProperty('format')) { - w.format(table, c, wo); - } - } - } - if (c.debug) { - benchmark("Completed " + (init === true ? "initializing" : "applying") + " widgets", time); - } - }; - - ts.refreshWidgets = function(table, doAll, dontapply) { - var i, c = table.config, - cw = c.widgets, - w = ts.widgets, l = w.length; - // remove previous widgets - for (i = 0; i < l; i++){ - if ( w[i] && w[i].id && (doAll || $.inArray( w[i].id, cw ) < 0) ) { - if (c.debug) { log( 'removing ' + w[i].id ); } - if (w[i].hasOwnProperty('remove')) { w[i].remove(table, c, c.widgetOptions); } - } - } - if (dontapply !== true) { - ts.applyWidget(table, doAll); - } - }; - - // get sorter, string, empty, etc options for each column from - // jQuery data, metadata, header option or header class name ("sorter-false") - // priority = jQuery data > meta > headers option > header class name - ts.getData = function(h, ch, key) { - var val = '', $h = $(h), m, cl; - if (!$h.length) { return ''; } - m = $.metadata ? $h.metadata() : false; - cl = ' ' + ($h.attr('class') || ''); - if (typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined'){ - // "data-lockedOrder" is assigned to "lockedorder"; but "data-locked-order" is assigned to "lockedOrder" - // "data-sort-initial-order" is assigned to "sortInitialOrder" - val += $h.data(key) || $h.data(key.toLowerCase()); - } else if (m && typeof m[key] !== 'undefined') { - val += m[key]; - } else if (ch && typeof ch[key] !== 'undefined') { - val += ch[key]; - } else if (cl !== ' ' && cl.match(' ' + key + '-')) { - // include sorter class name "sorter-text", etc - val = cl.match( new RegExp(' ' + key + '-(\\w+)') )[1] || ''; - } - return $.trim(val); - }; - - ts.formatFloat = function(s, table) { - if (typeof(s) !== 'string' || s === '') { return s; } - if (table.config.usNumberFormat !== false) { - // US Format - 1,234,567.89 -> 1234567.89 - s = s.replace(/,/g,''); - } else { - // German Format = 1.234.567,89 -> 1234567.89 - // French Format = 1 234 567,89 -> 1234567.89 - s = s.replace(/[\s|\.]/g,'').replace(/,/g,'.'); - } - if(/^\s*\([.\d]+\)/.test(s)) { - // make (#) into a negative number -> (10) = -10 - s = s.replace(/^\s*\(/,'-').replace(/\)/,''); - } - var i = parseFloat(s); - // return the text instead of zero - return isNaN(i) ? $.trim(s) : i; - }; - - ts.isDigit = function(s) { - // replace all unwanted chars and match - return isNaN(s) ? (/^[\-+(]?\d+[)]?$/).test(s.toString().replace(/[,.'\s]/g, '')) : true; - }; - - }() - }); - - // make shortcut - var ts = $.tablesorter; - - // extend plugin scope - $.fn.extend({ - tablesorter: ts.construct - }); - - // add default parsers - ts.addParser({ - id: "text", - is: function(s, table, node) { - return true; - }, - format: function(s, table, cell, cellIndex) { - var c = table.config; - s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s ); - return c.sortLocaleCompare ? ts.replaceAccents(s) : s; - }, - type: "text" - }); - - ts.addParser({ - id: "currency", - is: function(s) { - return (/^\(?[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+/).test(s); // £$€¤¥¢ - }, - format: function(s, table) { - return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "ipAddress", - is: function(s) { - return (/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/).test(s); - }, - format: function(s, table) { - var i, a = s.split("."), - r = "", - l = a.length; - for (i = 0; i < l; i++) { - r += ("00" + a[i]).slice(-3); - } - return ts.formatFloat(r, table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "url", - is: function(s) { - return (/^(https?|ftp|file):\/\//).test(s); - }, - format: function(s) { - return $.trim(s.replace(/(https?|ftp|file):\/\//, '')); - }, - type: "text" - }); - - ts.addParser({ - id: "isoDate", - is: function(s) { - return (/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/).test(s); - }, - format: function(s, table) { - return ts.formatFloat((s !== "") ? (new Date(s.replace(/-/g, "/")).getTime() || "") : "", table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "percent", - is: function(s) { - return (/\d%\)?$/).test(s); - }, - format: function(s, table) { - return ts.formatFloat(s.replace(/%/g, ""), table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "usLongDate", - is: function(s) { - return (/^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4}|'?\d{2})\s+(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i).test(s); - }, - format: function(s, table) { - return ts.formatFloat( (new Date(s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ''), table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "shortDate", // "mmddyyyy", "ddmmyyyy" or "yyyymmdd" - is: function(s) { - // testing for ####-##-#### - so it's not perfect - return (/^(\d{2}|\d{4})[\/\-\,\.\s+]\d{2}[\/\-\.\,\s+](\d{2}|\d{4})$/).test(s); - }, - format: function(s, table, cell, cellIndex) { - var c = table.config, ci = c.headerList[cellIndex], - format = ci.shortDateFormat; - if (typeof format === 'undefined') { - // cache header formatting so it doesn't getData for every cell in the column - format = ci.shortDateFormat = ts.getData( ci, c.headers[cellIndex], 'dateFormat') || c.dateFormat; - } - s = s.replace(/\s+/g," ").replace(/[\-|\.|\,]/g, "/"); - if (format === "mmddyyyy") { - s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$1/$2"); - } else if (format === "ddmmyyyy") { - s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$2/$1"); - } else if (format === "yyyymmdd") { - s = s.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/, "$1/$2/$3"); - } - return ts.formatFloat( (new Date(s).getTime() || ''), table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "time", - is: function(s) { - return (/^(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i).test(s); - }, - format: function(s, table) { - return ts.formatFloat( (new Date("2000/01/01 " + s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ""), table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "digit", - is: function(s) { - return ts.isDigit(s); - }, - format: function(s, table) { - return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); - }, - type: "numeric" - }); - - ts.addParser({ - id: "metadata", - is: function(s) { - return false; - }, - format: function(s, table, cell) { - var c = table.config, - p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; - return $(cell).metadata()[p]; - }, - type: "numeric" - }); - - // add default widgets - ts.addWidget({ - id: "zebra", - format: function(table, c, wo) { - var $tb, $tv, $tr, row, even, time, k, l, - child = new RegExp(c.cssChildRow, 'i'), - b = $(table).children('tbody:not(.' + c.cssInfoBlock + ')'); - if (c.debug) { - time = new Date(); - } - for (k = 0; k < b.length; k++ ) { - // loop through the visible rows - $tb = $(b[k]); - l = $tb.children('tr').length; - if (l > 1) { - row = 0; - $tv = $tb.children('tr:visible'); - // revered back to using jQuery each - strangely it's the fastest method - $tv.each(function(){ - $tr = $(this); - // style children rows the same way the parent row was styled - if (!child.test(this.className)) { row++; } - even = (row % 2 === 0); - $tr.removeClass(wo.zebra[even ? 1 : 0]).addClass(wo.zebra[even ? 0 : 1]); - }); - } - } - if (c.debug) { - ts.benchmark("Applying Zebra widget", time); - } - }, - remove: function(table, c, wo){ - var k, $tb, - b = $(table).children('tbody:not(.' + c.cssInfoBlock + ')'), - rmv = (c.widgetOptions.zebra || [ "even", "odd" ]).join(' '); - for (k = 0; k < b.length; k++ ){ - $tb = $.tablesorter.processTbody(table, $(b[k]), true); // remove tbody - $tb.children().removeClass(rmv); - $.tablesorter.processTbody(table, $tb, false); // restore tbody - } - } - }); - -})(jQuery); \ No newline at end of file diff --git a/sitestatic/jquery.tablesorter-2.4.5.min.js b/sitestatic/jquery.tablesorter-2.4.5.min.js deleted file mode 100644 index a52ce35f..00000000 --- a/sitestatic/jquery.tablesorter-2.4.5.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! -* TableSorter 2.4.5 - Client-side table sorting with ease! -* Minified using UglifyJS (http://jscompress.com/) -* Copyright (c) 2007 Christian Bach -*/ -!function($){"use strict";$.extend({tablesorter:new function(){function log(a){if(typeof console!=="undefined"&&typeof console.log!=="undefined"){console.log(a)}else{alert(a)}}function benchmark(a,b){log(a+" ("+((new Date).getTime()-b.getTime())+"ms)")}function getElementText(a,b,c){if(!b){return""}var d=a.config,e=d.textExtraction,f="";if(e==="simple"){if(d.supportsTextContent){f=b.textContent}else{f=$(b).text()}}else{if(typeof e==="function"){f=e(b,a,c)}else if(typeof e==="object"&&e.hasOwnProperty(c)){f=e[c](b,a,c)}else{f=d.supportsTextContent?b.textContent:$(b).text()}}return $.trim(f)}function detectParserForColumn(a,b,c,d){var e,f=ts.parsers.length,g=false,h="",i=true;while(h===""&&i){c++;if(b[c]){g=b[c].cells[d];h=getElementText(a,g,d);if(a.config.debug){log("Checking if value was empty on row "+c+", column: "+d+": "+h)}}else{i=false}}for(e=1;e
        ':"";this.innerHTML='
        '+this.innerHTML+e+"
        ";if(i.onRenderHeader){i.onRenderHeader.apply(d,[a])}this.column=b[this.parentNode.rowIndex+"-"+this.cellIndex];this.order=formatSortingOrder(ts.getData(d,c,"sortInitialOrder")||i.sortInitialOrder)?[1,0,2]:[0,1,2];this.count=-1;if(ts.getData(d,c,"sorter")==="false"){this.sortDisabled=true;d.addClass("sorter-false")}else{d.removeClass("sorter-false")}this.lockedOrder=false;f=ts.getData(d,c,"lockedOrder")||false;if(typeof f!=="undefined"&&f!==false){this.order=this.lockedOrder=formatSortingOrder(f)?[1,1,1]:[0,0,0]}d.addClass(this.sortDisabled?"sorter-false":i.cssHeader);i.headerList[a]=this;d.parent().addClass(i.cssHeaderRow)});if(a.config.debug){benchmark("Built headers:",g);log(h)}return h}function setHeadersCss(a,b){var c,d,e,f,g=a.config,h=g.sortList,i=[g.cssDesc,g.cssAsc],j=$(a).find("tfoot tr").children().removeClass(i.join(" "));b.removeClass(i.join(" "));f=h.length;for(d=0;d"),c=$(a).width();$("tr:first td",a.tBodies[0]).each(function(){b.append($("").css("width",parseInt($(this).width()/c*1e3,10)/10+"%"))});$(a).prepend(b)}}function updateHeaderSortCount(a,b){var c,d,e=a.config,f=e.headerList.length,g=b||e.sortList;e.sortList=[];$.each(g,function(a,b){c=[parseInt(b[0],10),parseInt(b[1],10)];d=e.headerList[c[0]];if(d){e.sortList.push(c);d.count=c[1]%(e.sortReset?3:2)}})}function getCachedSortType(a,b){return a&&a[b]?a[b].type||"":""}function multisort(table){var dynamicExp,sortWrapper,col,mx=0,dir=0,tc=table.config,sortList=tc.sortList,l=sortList.length,bl=table.tBodies.length,sortTime,i,j,k,c,cache,lc,s,e,order,orgOrderCol;if(tc.debug){sortTime=new Date}for(k=0;k thead th, > thead td",selectorSort:"th, td",selectorRemove:".remove-me",debug:false,headerList:[],empties:{},strings:{},parsers:[]};ts.benchmark=benchmark;ts.construct=function(a){return this.each(function(){if(!this.tHead||this.tBodies.length===0||this.hasInitialized===true){return}var b,c,d=$(this),e,f,g,h="",i,j,k,l,m=$.metadata;this.hasInitialized=false;this.config={};e=$.extend(true,this.config,ts.defaults,a);$.data(this,"tablesorter",e);if(e.debug){$.data(this,"startoveralltimer",new Date)}e.supportsTextContent=$("x")[0].textContent==="x";e.supportsDataObject=parseFloat($.fn.jquery)>=1.4;e.string={max:1,min:-1,"max+":1,"max-":-1,zero:0,none:0,"null":0,top:true,bottom:false};if(!/tablesorter\-/.test(d.attr("class"))){h=e.theme!==""?" tablesorter-"+e.theme:""}d.addClass(e.tableClass+h);b=buildHeaders(this);e.parsers=buildParserCache(this,b);if(!e.delayInit){buildCache(this)}b.find("*").andSelf().filter(e.selectorSort).unbind("mousedown.tablesorter mouseup.tablesorter").bind("mousedown.tablesorter mouseup.tablesorter",function(a,c){var m=this.tagName.match("TH|TD")?$(this):$(this).parents("th, td").filter(":last"),n=m[0];if((a.which||a.button)!==1){return false}if(a.type==="mousedown"){l=(new Date).getTime();return a.target.tagName==="INPUT"?"":!e.cancelSelection}if(c!==true&&(new Date).getTime()-l>250){return false}if(e.delayInit&&!e.cache){buildCache(d[0])}if(!n.sortDisabled){d.trigger("sortStart",d[0]);h=!a[e.sortMultiSortKey];n.count=(n.count+1)%(e.sortReset?3:2);if(e.sortRestart){f=n;b.each(function(){if(this!==f&&(h||!$(this).is("."+e.cssDesc+",."+e.cssAsc))){this.count=-1}})}f=n.column;if(h){e.sortList=[];if(e.sortForce!==null){i=e.sortForce;for(g=0;g1){for(g=1;g1){if(ts.isValueInArray(e.sortAppend[0][0],e.sortList)){e.sortList.pop()}}if(ts.isValueInArray(f,e.sortList)){for(g=0;g1){for(g=1;g=0){h=k.eq(l).find("tr").index(m);i=b.cellIndex;g=j.config.cache[l].normalized[h].length-1;j.config.cache[l].row[j.config.cache[l].normalized[h][g]]=m;j.config.cache[l].normalized[h][i]=e.parsers[i].format(getElementText(j,b,i),j,b,i);checkResort(d,c,f)}}).bind("addRows",function(a,b,c,f){var h,i=b.filter("tr").length,j=[],k=b[0].cells.length,l=this,m=$(this).find("tbody").index(b.closest("tbody"));for(h=0;h0){d.trigger("sorton",[e.sortList,{},!e.initWidgets])}else if(e.initWidgets){ts.applyWidget(this)}fixColumnWidth(this);if(e.showProcessing){d.unbind("sortBegin sortEnd").bind("sortBegin sortEnd",function(a){ts.isProcessing(d[0],a.type==="sortBegin")})}this.hasInitialized=true;if(e.debug){ts.benchmark("Overall initialization time",$.data(this,"startoveralltimer"))}d.trigger("tablesorter-initialized",this);if(typeof e.initialized==="function"){e.initialized(this)}})};ts.isProcessing=function(a,b,c){var d=a.config,e=c||$(a).find("."+d.cssHeader);if(b){if(d.sortList.length>0){e=e.filter(function(){return this.sortDisabled?false:ts.isValueInArray(parseFloat($(this).attr("data-column")),d.sortList)})}e.addClass(d.cssProcessing)}else{e.removeClass(d.cssProcessing)}};ts.processTbody=function(a,b,c){var d,e;if(c){b.before('');e=$.fn.detach?b.detach():b.remove();return e}e=$(a).find("span.tablesorter-savemyplace");b.insertAfter(e);e.remove()};ts.clearTableBody=function(a){$(a.tBodies).filter(":not(."+a.config.cssInfoBlock+")").empty()};ts.destroy=function(a,b,c){var d=$(a),e=a.config,f=d.find("thead:first");a.hasInitialized=false;f.find("tr:not(."+e.cssHeaderRow+")").remove();f.find(".tablesorter-resizer").remove();ts.refreshWidgets(a,true,true);d.removeData("tablesorter").unbind("update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave").find("."+e.cssHeader).unbind("click mousedown mousemove mouseup").removeClass(e.cssHeader+" "+e.cssAsc+" "+e.cssDesc).find(".tablesorter-header-inner").each(function(){if(e.cssIcon!==""){$(this).find("."+e.cssIcon).remove()}$(this).replaceWith($(this).contents())});if(b!==false){d.removeClass(e.tableClass)}if(typeof c==="function"){c(a)}};ts.regex=[/(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi,/(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,/^0x[0-9a-f]+$/i];ts.sortText=function(a,b,c,d){if(b===c){return 0}var e=a.config,f=e.string[e.empties[d]||e.emptyTo],g=ts.regex,h,i,j,k,l,m,n,o;if(b===""&&f!==0){return typeof f==="boolean"?f?-1:1:-f||-1}if(c===""&&f!==0){return typeof f==="boolean"?f?1:-1:f||1}if(typeof e.textSorter==="function"){return e.textSorter(b,c,a,d)}h=b.replace(g[0],"\\0$1\\0").replace(/\\0$/,"").replace(/^\\0/,"").split("\\0");j=c.replace(g[0],"\\0$1\\0").replace(/\\0$/,"").replace(/^\\0/,"").split("\\0");i=parseInt(b.match(g[2]),16)||h.length!==1&&b.match(g[1])&&Date.parse(b);k=parseInt(c.match(g[2]),16)||i&&c.match(g[1])&&Date.parse(c)||null;if(k){if(ik){return 1}}o=Math.max(h.length,j.length);for(n=0;nm){return 1}}return 0};ts.sortTextDesc=function(a,b,c,d){if(b===c){return 0}var e=a.config,f=e.string[e.empties[d]||e.emptyTo];if(b===""&&f!==0){return typeof f==="boolean"?f?-1:1:f||1}if(c===""&&f!==0){return typeof f==="boolean"?f?1:-1:-f||-1}if(typeof e.textSorter==="function"){return e.textSorter(c,b,a,d)}return ts.sortText(a,c,b)};ts.getTextValue=function(a,b,c){if(b){var d,e=a.length,f=b+c;for(d=0;d=0){c.widgets.splice(g,1);c.widgets.push("zebra")}if(c.debug){f=new Date}for(g=0;g1){g=0;e=d.children("tr:visible");e.each(function(){f=$(this);if(!l.test(this.className)){g++}h=g%2===0;f.removeClass(c.zebra[h?1:0]).addClass(c.zebra[h?0:1])})}}if(b.debug){ts.benchmark("Applying Zebra widget",i)}},remove:function(a,b,c){var d,e,f=$(a).children("tbody:not(."+b.cssInfoBlock+")"),g=(b.widgetOptions.zebra||["even","odd"]).join(" ");for(d=0;d (class from cssIcon) + onRenderTemplate : null, // function(index, template){ return template; }, (template is a string) + onRenderHeader : null, // function(index){}, (nothing to return) + + // *** functionality + cancelSelection : true, // prevent text selection in the header + dateFormat : 'mmddyyyy', // other options: "ddmmyyy" or "yyyymmdd" + sortMultiSortKey : 'shiftKey', // key used to select additional columns + sortResetKey : 'ctrlKey', // key used to remove sorting on a column + usNumberFormat : true, // false for German "1.234.567,89" or French "1 234 567,89" + delayInit : false, // if false, the parsed table contents will not update until the first sort + serverSideSorting: false, // if true, server-side sorting should be performed because client-side sorting will be disabled, but the ui and events will still be used. + + // *** sort options + headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc. + ignoreCase : true, // ignore case while sorting + sortForce : null, // column(s) first sorted; always applied + sortList : [], // Initial sort order; applied initially; updated when manually sorted + sortAppend : null, // column(s) sorted last; always applied + + sortInitialOrder : 'asc', // sort direction on first click + sortLocaleCompare: false, // replace equivalent character (accented characters) + sortReset : false, // third click on the header will reset column to default - unsorted + sortRestart : false, // restart sort to "sortInitialOrder" when clicking on previously unsorted columns + + emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero + stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero + textExtraction : 'simple', // text extraction method/function - function(node, table, cellIndex){} + textSorter : null, // use custom text sorter - function(a,b){ return a.sort(b); } // basic sort + + // *** widget options + widgets: [], // method to add widgets, e.g. widgets: ['zebra'] + widgetOptions : { + zebra : [ 'even', 'odd' ] // zebra widget alternating row class names + }, + initWidgets : true, // apply widgets on tablesorter initialization + + // *** callbacks + initialized : null, // function(table){}, + + // *** css class names + tableClass : 'tablesorter', + cssAsc : 'tablesorter-headerAsc', + cssChildRow : 'tablesorter-childRow', // previously "expand-child" + cssDesc : 'tablesorter-headerDesc', + cssHeader : 'tablesorter-header', + cssHeaderRow : 'tablesorter-headerRow', + cssIcon : 'tablesorter-icon', // if this class exists, a will be added to the header automatically + cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name + cssProcessing : 'tablesorter-processing', // processing icon applied to header during sort/filter + + // *** selectors + selectorHeaders : '> thead th, > thead td', + selectorSort : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort + selectorRemove : '.remove-me', + + // *** advanced + debug : false, + + // *** Internal variables + headerList: [], + empties: {}, + strings: {}, + parsers: [] + + // deprecated; but retained for backwards compatibility + // widgetZebra: { css: ["even", "odd"] } + + }; + + /* debuging utils */ + function log(s) { + if (typeof console !== "undefined" && typeof console.log !== "undefined") { + console.log(s); + } else { + alert(s); + } + } + + function benchmark(s, d) { + log(s + " (" + (new Date().getTime() - d.getTime()) + "ms)"); + } + + ts.benchmark = benchmark; + + function getElementText(table, node, cellIndex) { + if (!node) { return ""; } + var c = table.config, + t = c.textExtraction, text = ""; + if (t === "simple") { + if (c.supportsTextContent) { + text = node.textContent; // newer browsers support this + } else { + text = $(node).text(); + } + } else { + if (typeof(t) === "function") { + text = t(node, table, cellIndex); + } else if (typeof(t) === "object" && t.hasOwnProperty(cellIndex)) { + text = t[cellIndex](node, table, cellIndex); + } else { + text = c.supportsTextContent ? node.textContent : $(node).text(); + } + } + return $.trim(text); + } + + function detectParserForColumn(table, rows, rowIndex, cellIndex) { + var i, l = ts.parsers.length, + node = false, + nodeValue = '', + keepLooking = true; + while (nodeValue === '' && keepLooking) { + rowIndex++; + if (rows[rowIndex]) { + node = rows[rowIndex].cells[cellIndex]; + nodeValue = getElementText(table, node, cellIndex); + if (table.config.debug) { + log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': ' + nodeValue); + } + } else { + keepLooking = false; + } + } + for (i = 1; i < l; i++) { + if (ts.parsers[i].is(nodeValue, table, node)) { + return ts.parsers[i]; + } + } + // 0 is always the generic parser (text) + return ts.parsers[0]; + } + + function buildParserCache(table) { + var c = table.config, + tb = $(table.tBodies).filter(':not(.' + c.cssInfoBlock + ')'), + rows, list, l, i, h, ch, p, parsersDebug = ""; + if ( tb.length === 0) { + return c.debug ? log('*Empty table!* Not building a parser cache') : ''; + } + rows = tb[0].rows; + if (rows[0]) { + list = []; + l = rows[0].cells.length; + for (i = 0; i < l; i++) { + // tons of thanks to AnthonyM1229 for working out the following selector (issue #74) to make this work in IE8! + // More fixes to this selector to work properly in iOS and jQuery 1.8+ (issue #132 & #174) + h = c.$headers.filter(':not([colspan])'); + h = h.add( c.$headers.filter('[colspan="1"]') ) // ie8 fix + .filter('[data-column="' + i + '"]:last'); + ch = c.headers[i]; + // get column parser + p = ts.getParserById( ts.getData(h, ch, 'sorter') ); + // empty cells behaviour - keeping emptyToBottom for backwards compatibility + c.empties[i] = ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' ); + // text strings behaviour in numerical sorts + c.strings[i] = ts.getData(h, ch, 'string') || c.stringTo || 'max'; + if (!p) { + p = detectParserForColumn(table, rows, -1, i); + } + if (c.debug) { + parsersDebug += "column:" + i + "; parser:" + p.id + "; string:" + c.strings[i] + '; empty: ' + c.empties[i] + "\n"; + } + list.push(p); + } + } + if (c.debug) { + log(parsersDebug); + } + return list; + } + + /* utils */ + function buildCache(table) { + var b = table.tBodies, + tc = table.config, + totalRows, + totalCells, + parsers = tc.parsers, + t, v, i, j, k, c, cols, cacheTime, colMax = []; + tc.cache = {}; + // if no parsers found, return - it's an empty table. + if (!parsers) { + return tc.debug ? log('*Empty table!* Not building a cache') : ''; + } + if (tc.debug) { + cacheTime = new Date(); + } + // processing icon + if (tc.showProcessing) { + ts.isProcessing(table, true); + } + for (k = 0; k < b.length; k++) { + tc.cache[k] = { row: [], normalized: [] }; + // ignore tbodies with class name from css.cssInfoBlock + if (!$(b[k]).hasClass(tc.cssInfoBlock)) { + totalRows = (b[k] && b[k].rows.length) || 0; + totalCells = (b[k].rows[0] && b[k].rows[0].cells.length) || 0; + for (i = 0; i < totalRows; ++i) { + /** Add the table data to main data array */ + c = $(b[k].rows[i]); + cols = []; + // if this is a child row, add it to the last row's children and continue to the next row + if (c.hasClass(tc.cssChildRow)) { + tc.cache[k].row[tc.cache[k].row.length - 1] = tc.cache[k].row[tc.cache[k].row.length - 1].add(c); + // go to the next for loop + continue; + } + tc.cache[k].row.push(c); + for (j = 0; j < totalCells; ++j) { + t = getElementText(table, c[0].cells[j], j); + // allow parsing if the string is empty, previously parsing would change it to zero, + // in case the parser needs to extract data from the table cell attributes + v = parsers[j].format(t, table, c[0].cells[j], j); + cols.push(v); + if ((parsers[j].type || '').toLowerCase() === "numeric") { + colMax[j] = Math.max(Math.abs(v), colMax[j] || 0); // determine column max value (ignore sign) + } + } + cols.push(tc.cache[k].normalized.length); // add position for rowCache + tc.cache[k].normalized.push(cols); + } + tc.cache[k].colMax = colMax; + } + } + if (tc.showProcessing) { + ts.isProcessing(table); // remove processing icon + } + if (tc.debug) { + benchmark("Building cache for " + totalRows + " rows", cacheTime); + } + } + + // init flag (true) used by pager plugin to prevent widget application + function appendToTable(table, init) { + var c = table.config, + b = table.tBodies, + rows = [], + c2 = c.cache, + r, n, totalRows, checkCell, $bk, $tb, + i, j, k, l, pos, appendTime; + if (!c2[0]) { return; } // empty table - fixes #206 + if (c.debug) { + appendTime = new Date(); + } + for (k = 0; k < b.length; k++) { + $bk = $(b[k]); + if (!$bk.hasClass(c.cssInfoBlock)) { + // get tbody + $tb = ts.processTbody(table, $bk, true); + r = c2[k].row; + n = c2[k].normalized; + totalRows = n.length; + checkCell = totalRows ? (n[0].length - 1) : 0; + for (i = 0; i < totalRows; i++) { + pos = n[i][checkCell]; + rows.push(r[pos]); + // removeRows used by the pager plugin + if (!c.appender || !c.removeRows) { + l = r[pos].length; + for (j = 0; j < l; j++) { + $tb.append(r[pos][j]); + } + } + } + // restore tbody + ts.processTbody(table, $tb, false); + } + } + if (c.appender) { + c.appender(table, rows); + } + if (c.debug) { + benchmark("Rebuilt table", appendTime); + } + // apply table widgets + if (!init) { ts.applyWidget(table); } + // trigger sortend + $(table).trigger("sortEnd", table); + } + + // computeTableHeaderCellIndexes from: + // http://www.javascripttoolbox.com/lib/table/examples.php + // http://www.javascripttoolbox.com/temp/table_cellindex.html + function computeThIndexes(t) { + var matrix = [], + lookup = {}, + trs = $(t).find('thead:eq(0), tfoot').children('tr'), // children tr in tfoot - see issue #196 + i, j, k, l, c, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol, matrixrow; + for (i = 0; i < trs.length; i++) { + cells = trs[i].cells; + for (j = 0; j < cells.length; j++) { + c = cells[j]; + rowIndex = c.parentNode.rowIndex; + cellId = rowIndex + "-" + c.cellIndex; + rowSpan = c.rowSpan || 1; + colSpan = c.colSpan || 1; + if (typeof(matrix[rowIndex]) === "undefined") { + matrix[rowIndex] = []; + } + // Find first available column in the first row + for (k = 0; k < matrix[rowIndex].length + 1; k++) { + if (typeof(matrix[rowIndex][k]) === "undefined") { + firstAvailCol = k; + break; + } + } + lookup[cellId] = firstAvailCol; + // add data-column + $(c).attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex + for (k = rowIndex; k < rowIndex + rowSpan; k++) { + if (typeof(matrix[k]) === "undefined") { + matrix[k] = []; + } + matrixrow = matrix[k]; + for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) { + matrixrow[l] = "x"; + } + } + } + } + return lookup; + } + + function formatSortingOrder(v) { + // look for "d" in "desc" order; return true + return (/^d/i.test(v) || v === 1); + } + + function buildHeaders(table) { + var header_index = computeThIndexes(table), ch, $t, + h, i, t, lock, time, $tableHeaders, c = table.config; + c.headerList = [], c.headerContent = []; + if (c.debug) { + time = new Date(); + } + i = c.cssIcon ? '' : ''; // add icon if cssIcon option exists + $tableHeaders = $(table).find(c.selectorHeaders).each(function(index) { + $t = $(this); + ch = c.headers[index]; + c.headerContent[index] = this.innerHTML; // save original header content + // set up header template + t = c.headerTemplate.replace(/\{content\}/g, this.innerHTML).replace(/\{icon\}/g, i); + if (c.onRenderTemplate) { + h = c.onRenderTemplate.apply($t, [index, t]); + if (h && typeof h === 'string') { t = h; } // only change t if something is returned + } + this.innerHTML = '
        ' + t + '
        '; // faster than wrapInner + + if (c.onRenderHeader) { c.onRenderHeader.apply($t, [index]); } + + this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; + this.order = formatSortingOrder( ts.getData($t, ch, 'sortInitialOrder') || c.sortInitialOrder ) ? [1,0,2] : [0,1,2]; + this.count = -1; // set to -1 because clicking on the header automatically adds one + if (ts.getData($t, ch, 'sorter') === 'false') { + this.sortDisabled = true; + $t.addClass('sorter-false'); + } else { + $t.removeClass('sorter-false'); + } + this.lockedOrder = false; + lock = ts.getData($t, ch, 'lockedOrder') || false; + if (typeof(lock) !== 'undefined' && lock !== false) { + this.order = this.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0]; + } + $t.addClass( (this.sortDisabled ? 'sorter-false ' : ' ') + c.cssHeader ); + // add cell to headerList + c.headerList[index] = this; + // add to parent in case there are multiple rows + $t.parent().addClass(c.cssHeaderRow); + }); + if (table.config.debug) { + benchmark("Built headers:", time); + log($tableHeaders); + } + return $tableHeaders; + } + + function setHeadersCss(table) { + var f, i, j, l, + c = table.config, + list = c.sortList, + css = [c.cssAsc, c.cssDesc], + // find the footer + $t = $(table).find('tfoot tr').children().removeClass(css.join(' ')); + // remove all header information + c.$headers.removeClass(css.join(' ')); + l = list.length; + for (i = 0; i < l; i++) { + // direction = 2 means reset! + if (list[i][1] !== 2) { + // multicolumn sorting updating - choose the :last in case there are nested columns + f = c.$headers.not('.sorter-false').filter('[data-column="' + list[i][0] + '"]' + (l === 1 ? ':last' : '') ); + if (f.length) { + for (j = 0; j < f.length; j++) { + if (!f[j].sortDisabled) { + f.eq(j).addClass(css[list[i][1]]); + // add sorted class to footer, if it exists + if ($t.length) { + $t.filter('[data-column="' + list[i][0] + '"]').eq(j).addClass(css[list[i][1]]); + } + } + } + } + } + } + } + + function fixColumnWidth(table) { + if (table.config.widthFixed && $(table).find('colgroup').length === 0) { + var colgroup = $(''), + overallWidth = $(table).width(); + $("tr:first td", table.tBodies[0]).each(function() { + colgroup.append($('').css('width', parseInt(($(this).width()/overallWidth)*1000, 10)/10 + '%')); + }); + $(table).prepend(colgroup); + } + } + + function updateHeaderSortCount(table, list) { + var s, t, o, c = table.config, + sl = list || c.sortList; + c.sortList = []; + $.each(sl, function(i,v){ + // ensure all sortList values are numeric - fixes #127 + s = [ parseInt(v[0], 10), parseInt(v[1], 10) ]; + // make sure header exists + o = c.headerList[s[0]]; + if (o) { // prevents error if sorton array is wrong + c.sortList.push(s); + t = $.inArray(s[1], o.order); // fixes issue #167 + o.count = t >= 0 ? t : s[1] % (c.sortReset ? 3 : 2); + } + }); + } + + function getCachedSortType(parsers, i) { + return (parsers && parsers[i]) ? parsers[i].type || '' : ''; + } + + // sort multiple columns + function multisort(table) { /*jshint loopfunc:true */ + var dynamicExp, sortWrapper, col, mx = 0, dir = 0, tc = table.config, + sortList = tc.sortList, l = sortList.length, bl = table.tBodies.length, + sortTime, i, j, k, c, colMax, cache, lc, s, e, order, orgOrderCol; + if (tc.serverSideSorting || !tc.cache[0]) { // empty table - fixes #206 + return; + } + if (tc.debug) { sortTime = new Date(); } + for (k = 0; k < bl; k++) { + colMax = tc.cache[k].colMax; + cache = tc.cache[k].normalized; + lc = cache.length; + orgOrderCol = (cache && cache[0]) ? cache[0].length - 1 : 0; + cache.sort(function(a, b) { + // cache is undefined here in IE, so don't use it! + for (i = 0; i < l; i++) { + c = sortList[i][0]; + order = sortList[i][1]; + // fallback to natural sort since it is more robust + s = /n/i.test(getCachedSortType(tc.parsers, c)) ? "Numeric" : "Text"; + s += order === 0 ? "" : "Desc"; + if (/Numeric/.test(s) && tc.strings[c]) { + // sort strings in numerical columns + if (typeof (tc.string[tc.strings[c]]) === 'boolean') { + dir = (order === 0 ? 1 : -1) * (tc.string[tc.strings[c]] ? -1 : 1); + } else { + dir = (tc.strings[c]) ? tc.string[tc.strings[c]] || 0 : 0; + } + } + var sort = $.tablesorter["sort" + s](table, a[c], b[c], c, colMax[c], dir); + if (sort) { return sort; } + } + return a[orgOrderCol] - b[orgOrderCol]; + }); + } + if (tc.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time", sortTime); } + } + + function resortComplete($table, callback){ + $table.trigger('updateComplete'); + if (typeof callback === "function") { + callback($table[0]); + } + } + + function checkResort($table, flag, callback) { + if (flag !== false) { + $table.trigger("sorton", [$table[0].config.sortList, function(){ + resortComplete($table, callback); + }]); + } else { + resortComplete($table, callback); + } + } + + /* public methods */ + ts.construct = function(settings) { + return this.each(function() { + // if no thead or tbody, or tablesorter is already present, quit + if (!this.tHead || this.tBodies.length === 0 || this.hasInitialized === true) { + return (this.config.debug) ? log('stopping initialization! No thead, tbody or tablesorter has already been initialized') : ''; + } + // declare + var $cell, $this = $(this), + c, i, j, k = '', a, s, o, downTime, + m = $.metadata; + // initialization flag + this.hasInitialized = false; + // new blank config object + this.config = {}; + // merge and extend + c = $.extend(true, this.config, ts.defaults, settings); + // save the settings where they read + $.data(this, "tablesorter", c); + if (c.debug) { $.data( this, 'startoveralltimer', new Date()); } + // constants + c.supportsTextContent = $('x')[0].textContent === 'x'; + c.supportsDataObject = parseFloat($.fn.jquery) >= 1.4; + // digit sort text location; keeping max+/- for backwards compatibility + c.string = { 'max': 1, 'min': -1, 'max+': 1, 'max-': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false }; + // add table theme class only if there isn't already one there + if (!/tablesorter\-/.test($this.attr('class'))) { + k = (c.theme !== '' ? ' tablesorter-' + c.theme : ''); + } + $this.addClass(c.tableClass + k); + // build headers + c.$headers = buildHeaders(this); + // try to auto detect column type, and store in tables config + c.parsers = buildParserCache(this); + // build the cache for the tbody cells + // delayInit will delay building the cache until the user starts a sort + if (!c.delayInit) { buildCache(this); } + // apply event handling to headers + // this is to big, perhaps break it out? + c.$headers + // http://stackoverflow.com/questions/5312849/jquery-find-self + .find('*').andSelf().filter(c.selectorSort) + .unbind('mousedown.tablesorter mouseup.tablesorter') + .bind('mousedown.tablesorter mouseup.tablesorter', function(e, external) { + // jQuery v1.2.6 doesn't have closest() + var $cell = this.tagName.match('TH|TD') ? $(this) : $(this).parents('th, td').filter(':last'), cell = $cell[0]; + // only recognize left clicks + if ((e.which || e.button) !== 1) { return false; } + // set timer on mousedown + if (e.type === 'mousedown') { + downTime = new Date().getTime(); + return e.target.tagName === "INPUT" ? '' : !c.cancelSelection; + } + // ignore long clicks (prevents resizable widget from initializing a sort) + if (external !== true && (new Date().getTime() - downTime > 250)) { return false; } + if (c.delayInit && !c.cache) { buildCache($this[0]); } + if (!cell.sortDisabled) { + // Only call sortStart if sorting is enabled + $this.trigger("sortStart", $this[0]); + // store exp, for speed + // $cell = $(this); + k = !e[c.sortMultiSortKey]; + // get current column sort order + cell.count = e[c.sortResetKey] ? 2 : (cell.count + 1) % (c.sortReset ? 3 : 2); + // reset all sorts on non-current column - issue #30 + if (c.sortRestart) { + i = cell; + c.$headers.each(function() { + // only reset counts on columns that weren't just clicked on and if not included in a multisort + if (this !== i && (k || !$(this).is('.' + c.cssDesc + ',.' + c.cssAsc))) { + this.count = -1; + } + }); + } + // get current column index + i = cell.column; + // user only wants to sort on one column + if (k) { + // flush the sort list + c.sortList = []; + if (c.sortForce !== null) { + a = c.sortForce; + for (j = 0; j < a.length; j++) { + if (a[j][0] !== i) { + c.sortList.push(a[j]); + } + } + } + // add column to sort list + o = cell.order[cell.count]; + if (o < 2) { + c.sortList.push([i, o]); + // add other columns if header spans across multiple + if (cell.colSpan > 1) { + for (j = 1; j < cell.colSpan; j++) { + c.sortList.push([i + j, o]); + } + } + } + // multi column sorting + } else { + // get rid of the sortAppend before adding more - fixes issue #115 + if (c.sortAppend && c.sortList.length > 1) { + if (ts.isValueInArray(c.sortAppend[0][0], c.sortList)) { + c.sortList.pop(); + } + } + // the user has clicked on an already sorted column + if (ts.isValueInArray(i, c.sortList)) { + // reverse the sorting direction for all tables + for (j = 0; j < c.sortList.length; j++) { + s = c.sortList[j]; + o = c.headerList[s[0]]; + if (s[0] === i) { + s[1] = o.order[o.count]; + if (s[1] === 2) { + c.sortList.splice(j,1); + o.count = -1; + } + } + } + } else { + // add column to sort list array + o = cell.order[cell.count]; + if (o < 2) { + c.sortList.push([i, o]); + // add other columns if header spans across multiple + if (cell.colSpan > 1) { + for (j = 1; j < cell.colSpan; j++) { + c.sortList.push([i + j, o]); + } + } + } + } + } + if (c.sortAppend !== null) { + a = c.sortAppend; + for (j = 0; j < a.length; j++) { + if (a[j][0] !== i) { + c.sortList.push(a[j]); + } + } + } + // sortBegin event triggered immediately before the sort + $this.trigger("sortBegin", $this[0]); + // setTimeout needed so the processing icon shows up + setTimeout(function(){ + // set css for headers + setHeadersCss($this[0]); + multisort($this[0]); + appendToTable($this[0]); + }, 1); + } + }); + if (c.cancelSelection) { + // cancel selection + c.$headers.each(function() { + this.onselectstart = function() { + return false; + }; + }); + } + // apply easy methods that trigger binded events + $this + .unbind('sortReset update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave') + .bind("sortReset", function(){ + c.sortList = []; + setHeadersCss(this); + multisort(this); + appendToTable(this); + }) + .bind("update", function(e, resort, callback) { + // remove rows/elements before update + $(c.selectorRemove, this).remove(); + // rebuild parsers + c.parsers = buildParserCache(this); + // rebuild the cache map + buildCache(this); + checkResort($this, resort, callback); + }) + .bind("updateCell", function(e, cell, resort, callback) { + // get position from the dom + var l, row, icell, + t = this, $tb = $(this).find('tbody'), + // update cache - format: function(s, table, cell, cellIndex) + // no closest in jQuery v1.2.6 - tbdy = $tb.index( $(cell).closest('tbody') ),$row = $(cell).closest('tr'); + tbdy = $tb.index( $(cell).parents('tbody').filter(':last') ), + $row = $(cell).parents('tr').filter(':last'); + cell = $(cell)[0]; // in case cell is a jQuery object + // tbody may not exist if update is initialized while tbody is removed for processing + if ($tb.length && tbdy >= 0) { + row = $tb.eq(tbdy).find('tr').index( $row ); + icell = cell.cellIndex; + l = t.config.cache[tbdy].normalized[row].length - 1; + t.config.cache[tbdy].row[t.config.cache[tbdy].normalized[row][l]] = $row; + t.config.cache[tbdy].normalized[row][icell] = c.parsers[icell].format( getElementText(t, cell, icell), t, cell, icell ); + checkResort($this, resort, callback); + } + }) + .bind("addRows", function(e, $row, resort, callback) { + var i, rows = $row.filter('tr').length, + dat = [], l = $row[0].cells.length, t = this, + tbdy = $(this).find('tbody').index( $row.closest('tbody') ); + // fixes adding rows to an empty table - see issue #179 + if (!c.parsers) { + c.parsers = buildParserCache(t); + } + // add each row + for (i = 0; i < rows; i++) { + // add each cell + for (j = 0; j < l; j++) { + dat[j] = c.parsers[j].format( getElementText(t, $row[i].cells[j], j), t, $row[i].cells[j], j ); + } + // add the row index to the end + dat.push(c.cache[tbdy].row.length); + // update cache + c.cache[tbdy].row.push([$row[i]]); + c.cache[tbdy].normalized.push(dat); + dat = []; + } + // resort using current settings + checkResort($this, resort, callback); + }) + .bind("sorton", function(e, list, callback, init) { + $(this).trigger("sortStart", this); + // update header count index + updateHeaderSortCount(this, list); + // set css for headers + setHeadersCss(this); + // sort the table and append it to the dom + multisort(this); + appendToTable(this, init); + if (typeof callback === "function") { + callback(this); + } + }) + .bind("appendCache", function(e, callback, init) { + appendToTable(this, init); + if (typeof callback === "function") { + callback(this); + } + }) + .bind("applyWidgetId", function(e, id) { + ts.getWidgetById(id).format(this, c, c.widgetOptions); + }) + .bind("applyWidgets", function(e, init) { + // apply widgets + ts.applyWidget(this, init); + }) + .bind("refreshWidgets", function(e, all, dontapply){ + ts.refreshWidgets(this, all, dontapply); + }) + .bind("destroy", function(e, c, cb){ + ts.destroy(this, c, cb); + }); + + // get sort list from jQuery data or metadata + // in jQuery < 1.4, an error occurs when calling $this.data() + if (c.supportsDataObject && typeof $this.data().sortlist !== 'undefined') { + c.sortList = $this.data().sortlist; + } else if (m && ($this.metadata() && $this.metadata().sortlist)) { + c.sortList = $this.metadata().sortlist; + } + // apply widget init code + ts.applyWidget(this, true); + // if user has supplied a sort list to constructor + if (c.sortList.length > 0) { + $this.trigger("sorton", [c.sortList, {}, !c.initWidgets]); + } else if (c.initWidgets) { + // apply widget format + ts.applyWidget(this); + } + + // fixate columns if the users supplies the fixedWidth option + // do this after theme has been applied + fixColumnWidth(this); + + // show processesing icon + if (c.showProcessing) { + $this + .unbind('sortBegin sortEnd') + .bind('sortBegin sortEnd', function(e) { + ts.isProcessing($this[0], e.type === 'sortBegin'); + }); + } + + // initialized + this.hasInitialized = true; + if (c.debug) { + ts.benchmark("Overall initialization time", $.data( this, 'startoveralltimer')); + } + $this.trigger('tablesorter-initialized', this); + if (typeof c.initialized === 'function') { c.initialized(this); } + }); + }; + + // *** Process table *** + // add processing indicator + ts.isProcessing = function(table, toggle, $ths) { + var c = table.config, + // default to all headers + $h = $ths || $(table).find('.' + c.cssHeader); + if (toggle) { + if (c.sortList.length > 0) { + // get headers from the sortList + $h = $h.filter(function(){ + // get data-column from attr to keep compatibility with jQuery 1.2.6 + return this.sortDisabled ? false : ts.isValueInArray( parseFloat($(this).attr('data-column')), c.sortList); + }); + } + $h.addClass(c.cssProcessing); + } else { + $h.removeClass(c.cssProcessing); + } + }; + + // detach tbody but save the position + // don't use tbody because there are portions that look for a tbody index (updateCell) + ts.processTbody = function(table, $tb, getIt){ + var t, holdr; + if (getIt) { + $tb.before(''); + holdr = ($.fn.detach) ? $tb.detach() : $tb.remove(); + return holdr; + } + holdr = $(table).find('span.tablesorter-savemyplace'); + $tb.insertAfter( holdr ); + holdr.remove(); + }; + + ts.clearTableBody = function(table) { + $(table.tBodies).filter(':not(.' + table.config.cssInfoBlock + ')').empty(); + }; + + ts.destroy = function(table, removeClasses, callback){ + var $t = $(table), c = table.config, + $h = $t.find('thead:first'); + // clear flag in case the plugin is initialized again + table.hasInitialized = false; + // remove widget added rows + $h.find('tr:not(.' + c.cssHeaderRow + ')').remove(); + // remove resizer widget stuff + $h.find('.tablesorter-resizer').remove(); + // remove all widgets + ts.refreshWidgets(table, true, true); + // disable tablesorter + $t + .removeData('tablesorter') + .unbind('sortReset update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave') + .find('.' + c.cssHeader) + .unbind('click mousedown mousemove mouseup') + .removeClass(c.cssHeader + ' ' + c.cssAsc + ' ' + c.cssDesc) + .find('.tablesorter-header-inner').each(function(){ + if (c.cssIcon !== '') { $(this).find('.' + c.cssIcon).remove(); } + $(this).replaceWith( $(this).contents() ); + }); + if (removeClasses !== false) { + $t.removeClass(c.tableClass); + } + if (typeof callback === 'function') { + callback(table); + } + }; + + // *** sort functions *** + // regex used in natural sort + ts.regex = [ + /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi, // chunk/tokenize numbers & letters + /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/, //date + /^0x[0-9a-f]+$/i // hex + ]; + + // Natural sort - https://github.com/overset/javascript-natural-sort + ts.sortText = function(table, a, b, col) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ], + r = ts.regex, xN, xD, yN, yD, xF, yF, i, mx; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } + if (typeof c.textSorter === 'function') { return c.textSorter(a, b, table, col); } + // chunk/tokenize + xN = a.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); + yN = b.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); + // numeric, hex or date detection + xD = parseInt(a.match(r[2]),16) || (xN.length !== 1 && a.match(r[1]) && Date.parse(a)); + yD = parseInt(b.match(r[2]),16) || (xD && b.match(r[1]) && Date.parse(b)) || null; + // first try and sort Hex codes or Dates + if (yD) { + if ( xD < yD ) { return -1; } + if ( xD > yD ) { return 1; } + } + mx = Math.max(xN.length, yN.length); + // natural sorting through split numeric strings and default strings + for (i = 0; i < mx; i++) { + // find floats not starting with '0', string or 0 if not defined + xF = isNaN(xN[i]) ? xN[i] || 0 : parseFloat(xN[i]) || 0; + yF = isNaN(yN[i]) ? yN[i] || 0 : parseFloat(yN[i]) || 0; + // handle numeric vs string comparison - number < string - (Kyle Adams) + if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; } + // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' + if (typeof xF !== typeof yF) { + xF += ''; + yF += ''; + } + if (xF < yF) { return -1; } + if (xF > yF) { return 1; } + } + return 0; + }; + + ts.sortTextDesc = function(table, a, b, col) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } + if (typeof c.textSorter === 'function') { return c.textSorter(b, a, table, col); } + return ts.sortText(table, b, a); + }; + + // return text string value by adding up ascii value + // so the text is somewhat sorted when using a digital sort + // this is NOT an alphanumeric sort + ts.getTextValue = function(a, mx, d) { + if (mx) { + // make sure the text value is greater than the max numerical value (mx) + var i, l = a.length, n = mx + d; + for (i = 0; i < l; i++) { + n += a.charCodeAt(i); + } + return d * n; + } + return 0; + }; + + ts.sortNumeric = function(table, a, b, col, mx, d) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } + if (isNaN(a)) { a = ts.getTextValue(a, mx, d); } + if (isNaN(b)) { b = ts.getTextValue(b, mx, d); } + return a - b; + }; + + ts.sortNumericDesc = function(table, a, b, col, mx, d) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } + if (isNaN(a)) { a = ts.getTextValue(a, mx, d); } + if (isNaN(b)) { b = ts.getTextValue(b, mx, d); } + return b - a; + }; + + // used when replacing accented characters during sorting + ts.characterEquivalents = { + "a" : "\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5", // áàâãäąå + "A" : "\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5", // ÁÀÂÃÄĄÅ + "c" : "\u00e7\u0107\u010d", // çćč + "C" : "\u00c7\u0106\u010c", // ÇĆČ + "e" : "\u00e9\u00e8\u00ea\u00eb\u011b\u0119", // éèêëěę + "E" : "\u00c9\u00c8\u00ca\u00cb\u011a\u0118", // ÉÈÊËĚĘ + "i" : "\u00ed\u00ec\u0130\u00ee\u00ef\u0131", // íìİîïı + "I" : "\u00cd\u00cc\u0130\u00ce\u00cf", // ÍÌİÎÏ + "o" : "\u00f3\u00f2\u00f4\u00f5\u00f6", // óòôõö + "O" : "\u00d3\u00d2\u00d4\u00d5\u00d6", // ÓÒÔÕÖ + "ss": "\u00df", // ß (s sharp) + "SS": "\u1e9e", // ẞ (Capital sharp s) + "u" : "\u00fa\u00f9\u00fb\u00fc\u016f", // úùûüů + "U" : "\u00da\u00d9\u00db\u00dc\u016e" // ÚÙÛÜŮ + }; + ts.replaceAccents = function(s) { + var a, acc = '[', eq = ts.characterEquivalents; + if (!ts.characterRegex) { + ts.characterRegexArray = {}; + for (a in eq) { + if (typeof a === 'string') { + acc += eq[a]; + ts.characterRegexArray[a] = new RegExp('[' + eq[a] + ']', 'g'); + } + } + ts.characterRegex = new RegExp(acc + ']'); + } + if (ts.characterRegex.test(s)) { + for (a in eq) { + if (typeof a === 'string') { + s = s.replace( ts.characterRegexArray[a], a ); + } + } + } + return s; + }; + + // *** utilities *** + ts.isValueInArray = function(v, a) { + var i, l = a.length; + for (i = 0; i < l; i++) { + if (a[i][0] === v) { + return true; + } + } + return false; + }; + + ts.addParser = function(parser) { + var i, l = ts.parsers.length, a = true; + for (i = 0; i < l; i++) { + if (ts.parsers[i].id.toLowerCase() === parser.id.toLowerCase()) { + a = false; + } + } + if (a) { + ts.parsers.push(parser); + } + }; + + ts.getParserById = function(name) { + var i, l = ts.parsers.length; + for (i = 0; i < l; i++) { + if (ts.parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) { + return ts.parsers[i]; + } + } + return false; + }; + + ts.addWidget = function(widget) { + ts.widgets.push(widget); + }; + + ts.getWidgetById = function(name) { + var i, w, l = ts.widgets.length; + for (i = 0; i < l; i++) { + w = ts.widgets[i]; + if (w && w.hasOwnProperty('id') && w.id.toLowerCase() === name.toLowerCase()) { + return w; + } + } + }; + + ts.applyWidget = function(table, init) { + var c = table.config, + wo = c.widgetOptions, + ws = c.widgets.sort().reverse(), // ensure that widgets are always applied in a certain order + time, i, w, l = ws.length; + // make zebra last + i = $.inArray('zebra', c.widgets); + if (i >= 0) { + c.widgets.splice(i,1); + c.widgets.push('zebra'); + } + if (c.debug) { + time = new Date(); + } + // add selected widgets + for (i = 0; i < l; i++) { + w = ts.getWidgetById(ws[i]); + if ( w ) { + if (init === true && w.hasOwnProperty('init')) { + w.init(table, w, c, wo); + } else if (!init && w.hasOwnProperty('format')) { + w.format(table, c, wo); + } + } + } + if (c.debug) { + benchmark("Completed " + (init === true ? "initializing" : "applying") + " widgets", time); + } + }; + + ts.refreshWidgets = function(table, doAll, dontapply) { + var i, c = table.config, + cw = c.widgets, + w = ts.widgets, l = w.length; + // remove previous widgets + for (i = 0; i < l; i++){ + if ( w[i] && w[i].id && (doAll || $.inArray( w[i].id, cw ) < 0) ) { + if (c.debug) { log( 'Refeshing widgets: Removing ' + w[i].id ); } + if (w[i].hasOwnProperty('remove')) { w[i].remove(table, c, c.widgetOptions); } + } + } + if (dontapply !== true) { + ts.applyWidget(table, doAll); + } + }; + + // get sorter, string, empty, etc options for each column from + // jQuery data, metadata, header option or header class name ("sorter-false") + // priority = jQuery data > meta > headers option > header class name + ts.getData = function(h, ch, key) { + var val = '', $h = $(h), m, cl; + if (!$h.length) { return ''; } + m = $.metadata ? $h.metadata() : false; + cl = ' ' + ($h.attr('class') || ''); + if (typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined'){ + // "data-lockedOrder" is assigned to "lockedorder"; but "data-locked-order" is assigned to "lockedOrder" + // "data-sort-initial-order" is assigned to "sortInitialOrder" + val += $h.data(key) || $h.data(key.toLowerCase()); + } else if (m && typeof m[key] !== 'undefined') { + val += m[key]; + } else if (ch && typeof ch[key] !== 'undefined') { + val += ch[key]; + } else if (cl !== ' ' && cl.match(' ' + key + '-')) { + // include sorter class name "sorter-text", etc + val = cl.match( new RegExp(' ' + key + '-(\\w+)') )[1] || ''; + } + return $.trim(val); + }; + + ts.formatFloat = function(s, table) { + if (typeof(s) !== 'string' || s === '') { return s; } + // allow using formatFloat without a table; defaults to US number format + var i, + t = table && table.config ? table.config.usNumberFormat !== false : + typeof table !== "undefined" ? table : true; + if (t) { + // US Format - 1,234,567.89 -> 1234567.89 + s = s.replace(/,/g,''); + } else { + // German Format = 1.234.567,89 -> 1234567.89 + // French Format = 1 234 567,89 -> 1234567.89 + s = s.replace(/[\s|\.]/g,'').replace(/,/g,'.'); + } + if(/^\s*\([.\d]+\)/.test(s)) { + // make (#) into a negative number -> (10) = -10 + s = s.replace(/^\s*\(/,'-').replace(/\)/,''); + } + i = parseFloat(s); + // return the text instead of zero + return isNaN(i) ? $.trim(s) : i; + }; + + ts.isDigit = function(s) { + // replace all unwanted chars and match + return isNaN(s) ? (/^[\-+(]?\d+[)]?$/).test(s.toString().replace(/[,.'"\s]/g, '')) : true; + }; + + }() + }); + + // make shortcut + var ts = $.tablesorter; + + // extend plugin scope + $.fn.extend({ + tablesorter: ts.construct + }); + + // add default parsers + ts.addParser({ + id: "text", + is: function(s, table, node) { + return true; + }, + format: function(s, table, cell, cellIndex) { + var c = table.config; + s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s ); + return c.sortLocaleCompare ? ts.replaceAccents(s) : s; + }, + type: "text" + }); + + ts.addParser({ + id: "currency", + is: function(s) { + return (/^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/).test(s); // £$€¤¥¢ + }, + format: function(s, table) { + return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "ipAddress", + is: function(s) { + return (/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/).test(s); + }, + format: function(s, table) { + var i, a = s.split("."), + r = "", + l = a.length; + for (i = 0; i < l; i++) { + r += ("00" + a[i]).slice(-3); + } + return ts.formatFloat(r, table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "url", + is: function(s) { + return (/^(https?|ftp|file):\/\//).test(s); + }, + format: function(s) { + return $.trim(s.replace(/(https?|ftp|file):\/\//, '')); + }, + type: "text" + }); + + ts.addParser({ + id: "isoDate", + is: function(s) { + return (/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/).test(s); + }, + format: function(s, table) { + return ts.formatFloat((s !== "") ? (new Date(s.replace(/-/g, "/")).getTime() || "") : "", table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "percent", + is: function(s) { + return (/(\d\s?%|%\s?\d)/).test(s); + }, + format: function(s, table) { + return ts.formatFloat(s.replace(/%/g, ""), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "usLongDate", + is: function(s) { + // two digit years are not allowed cross-browser + return (/^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i).test(s); + }, + format: function(s, table) { + return ts.formatFloat( (new Date(s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ''), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "shortDate", // "mmddyyyy", "ddmmyyyy" or "yyyymmdd" + is: function(s) { + // testing for ####-##-####, so it's not perfect + return (/^(\d{1,2}|\d{4})[\/\-\,\.\s+]\d{1,2}[\/\-\.\,\s+](\d{1,2}|\d{4})$/).test(s); + }, + format: function(s, table, cell, cellIndex) { + var c = table.config, ci = c.headerList[cellIndex], + format = ci.shortDateFormat; + if (typeof format === 'undefined') { + // cache header formatting so it doesn't getData for every cell in the column + format = ci.shortDateFormat = ts.getData( ci, c.headers[cellIndex], 'dateFormat') || c.dateFormat; + } + s = s.replace(/\s+/g," ").replace(/[\-|\.|\,]/g, "/"); + if (format === "mmddyyyy") { + s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$1/$2"); + } else if (format === "ddmmyyyy") { + s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$2/$1"); + } else if (format === "yyyymmdd") { + s = s.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/, "$1/$2/$3"); + } + return ts.formatFloat( (new Date(s).getTime() || ''), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "time", + is: function(s) { + return (/^(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i).test(s); + }, + format: function(s, table) { + return ts.formatFloat( (new Date("2000/01/01 " + s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ""), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "digit", + is: function(s) { + return ts.isDigit(s); + }, + format: function(s, table) { + return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); + }, + type: "numeric" + }); + + ts.addParser({ + id: "metadata", + is: function(s) { + return false; + }, + format: function(s, table, cell) { + var c = table.config, + p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; + return $(cell).metadata()[p]; + }, + type: "numeric" + }); + + // add default widgets + ts.addWidget({ + id: "zebra", + format: function(table, c, wo) { + var $tb, $tv, $tr, row, even, time, k, l, + child = new RegExp(c.cssChildRow, 'i'), + b = $(table).children('tbody:not(.' + c.cssInfoBlock + ')'); + if (c.debug) { + time = new Date(); + } + for (k = 0; k < b.length; k++ ) { + // loop through the visible rows + $tb = $(b[k]); + l = $tb.children('tr').length; + if (l > 1) { + row = 0; + $tv = $tb.children('tr:visible'); + // revered back to using jQuery each - strangely it's the fastest method + /*jshint loopfunc:true */ + $tv.each(function(){ + $tr = $(this); + // style children rows the same way the parent row was styled + if (!child.test(this.className)) { row++; } + even = (row % 2 === 0); + $tr.removeClass(wo.zebra[even ? 1 : 0]).addClass(wo.zebra[even ? 0 : 1]); + }); + } + } + if (c.debug) { + ts.benchmark("Applying Zebra widget", time); + } + }, + remove: function(table, c, wo){ + var k, $tb, + b = $(table).children('tbody:not(.' + c.cssInfoBlock + ')'), + rmv = (c.widgetOptions.zebra || [ "even", "odd" ]).join(' '); + for (k = 0; k < b.length; k++ ){ + $tb = $.tablesorter.processTbody(table, $(b[k]), true); // remove tbody + $tb.children().removeClass(rmv); + $.tablesorter.processTbody(table, $tb, false); // restore tbody + } + } + }); + +})(jQuery); \ No newline at end of file diff --git a/sitestatic/jquery.tablesorter-2.7.min.js b/sitestatic/jquery.tablesorter-2.7.min.js new file mode 100644 index 00000000..cf5e3068 --- /dev/null +++ b/sitestatic/jquery.tablesorter-2.7.min.js @@ -0,0 +1,5 @@ +/*! +* TableSorter 2.7 min - Client-side table sorting with ease! +* Copyright (c) 2007 Christian Bach +*/ +!function(g){g.extend({tablesorter:new function(){function d(c){"undefined"!==typeof console&&"undefined"!==typeof console.log?console.log(c):alert(c)}function v(c,b){d(c+" ("+((new Date).getTime()-b.getTime())+"ms)")}function p(c,b,a){if(!b)return"";var f=c.config,h=f.textExtraction,e="",e="simple"===h?f.supportsTextContent?b.textContent:g(b).text():"function"===typeof h?h(b,c,a):"object"===typeof h&&h.hasOwnProperty(a)?h[a](b,c,a):f.supportsTextContent?b.textContent:g(b).text();return g.trim(e)} function k(c){var b=c.config,a=g(c.tBodies).filter(":not(."+b.cssInfoBlock+")"),f,h,s,j,m,l,n="";if(0===a.length)return b.debug?d("*Empty table!* Not building a parser cache"):"";a=a[0].rows;if(a[0]){f=[];h=a[0].cells.length;for(s=0;s thead th, > thead td",selectorSort:"th, td",selectorRemove:".remove-me",debug:!1,headerList:[],empties:{},strings:{},parsers:[]};e.benchmark=v;e.construct=function(c){return this.each(function(){if(!this.tHead||0===this.tBodies.length||!0===this.hasInitialized)return this.config.debug?d("stopping initialization! No thead, tbody or tablesorter has already been initialized"):"";var b=g(this),a,f,h,s="",j,m,l,n,D=g.metadata;this.hasInitialized= !1;this.config={};a=g.extend(!0,this.config,e.defaults,c);g.data(this,"tablesorter",a);a.debug&&g.data(this,"startoveralltimer",new Date);a.supportsTextContent="x"===g("x")[0].textContent;a.supportsDataObject=1.4<=parseFloat(g.fn.jquery);a.string={max:1,min:-1,"max+":1,"max-":-1,zero:0,none:0,"null":0,top:!0,bottom:!1};/tablesorter\-/.test(b.attr("class"))||(s=""!==a.theme?" tablesorter-"+a.theme:"");b.addClass(a.tableClass+s);var r=[],P={},y=g(this).find("thead:eq(0), tfoot").children("tr"), I,J,x,z,N,B,K,Q,R,G;for(I=0;I
        ':"";r=g(this).find(w.selectorHeaders).each(function(a){A=g(this);L=w.headers[a];w.headerContent[a]=this.innerHTML;M=w.headerTemplate.replace(/\{content\}/g,this.innerHTML).replace(/\{icon\}/g,S);w.onRenderTemplate&&(O=w.onRenderTemplate.apply(A,[a,M]))&&"string"===typeof O&&(M=O);this.innerHTML='
        '+M+"
        ";w.onRenderHeader&&w.onRenderHeader.apply(A,[a]);this.column=P[this.parentNode.rowIndex+"-"+this.cellIndex];var b=e.getData(A,L,"sortInitialOrder")|| w.sortInitialOrder;this.order=/^d/i.test(b)||1===b?[1,0,2]:[0,1,2];this.count=-1;"false"===e.getData(A,L,"sorter")?(this.sortDisabled=!0,A.addClass("sorter-false")):A.removeClass("sorter-false");this.lockedOrder=!1;H=e.getData(A,L,"lockedOrder")||!1;"undefined"!==typeof H&&!1!==H&&(this.order=this.lockedOrder=/^d/i.test(H)||1===H?[1,1,1]:[0,0,0]);A.addClass((this.sortDisabled?"sorter-false ":" ")+w.cssHeader);w.headerList[a]=this;A.parent().addClass(w.cssHeaderRow)});this.config.debug&&(v("Built headers:", T),d(r));a.$headers=r;a.parsers=k(this);a.delayInit||q(this);a.$headers.find("*").andSelf().filter(a.selectorSort).unbind("mousedown.tablesorter mouseup.tablesorter").bind("mousedown.tablesorter mouseup.tablesorter",function(c,d){var k=(this.tagName.match("TH|TD")?g(this):g(this).parents("th, td").filter(":last"))[0];if(1!==(c.which||c.button))return!1;if("mousedown"===c.type)return n=(new Date).getTime(),"INPUT"===c.target.tagName?"":!a.cancelSelection;if(!0!==d&&250<(new Date).getTime()-n)return!1; a.delayInit&&!a.cache&&q(b[0]);if(!k.sortDisabled){b.trigger("sortStart",b[0]);s=!c[a.sortMultiSortKey];k.count=c[a.sortResetKey]?2:(k.count+1)%(a.sortReset?3:2);a.sortRestart&&(f=k,a.$headers.each(function(){if(this!==f&&(s||!g(this).is("."+a.cssDesc+",."+a.cssAsc)))this.count=-1}));f=k.column;if(s){a.sortList=[];if(null!==a.sortForce){j=a.sortForce;for(h=0;hl&&(a.sortList.push([f,l]),1l&&(a.sortList.push([f,l]),1"),V=g(this).width();g("tr:first td",this.tBodies[0]).each(function(){U.append(g("").css("width",parseInt(1E3*(g(this).width()/V),10)/10+"%"))});g(this).prepend(U)}a.showProcessing&&b.unbind("sortBegin sortEnd").bind("sortBegin sortEnd", function(a){e.isProcessing(b[0],"sortBegin"===a.type)});this.hasInitialized=!0;a.debug&&e.benchmark("Overall initialization time",g.data(this,"startoveralltimer"));b.trigger("tablesorter-initialized",this);"function"===typeof a.initialized&&a.initialized(this)})};e.isProcessing=function(c,b,a){var f=c.config;c=a||g(c).find("."+f.cssHeader);b?(0'),c=g.fn.detach?b.detach():b.remove();c=g(c).find("span.tablesorter-savemyplace");b.insertAfter(c);c.remove()};e.clearTableBody=function(c){g(c.tBodies).filter(":not(."+c.config.cssInfoBlock+")").empty()};e.destroy=function(c,b,a){var f=g(c),h=c.config,d=f.find("thead:first");c.hasInitialized=!1;d.find("tr:not(."+h.cssHeaderRow+")").remove();d.find(".tablesorter-resizer").remove(); e.refreshWidgets(c,!0,!0);f.removeData("tablesorter").unbind("sortReset update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave").find("."+h.cssHeader).unbind("click mousedown mousemove mouseup").removeClass(h.cssHeader+" "+h.cssAsc+" "+h.cssDesc).find(".tablesorter-header-inner").each(function(){""!==h.cssIcon&&g(this).find("."+h.cssIcon).remove();g(this).replaceWith(g(this).contents())});!1!==b&&f.removeClass(h.tableClass);"function"===typeof a&& a(c)};e.regex=[/(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi,/(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,/^0x[0-9a-f]+$/i];e.sortText=function(c,b,a,f){if(b===a)return 0;var h=c.config,d=h.string[h.empties[f]||h.emptyTo],j=e.regex;if(""===b&&0!==d)return"boolean"===typeof d?d?-1:1:-d||-1;if(""===a&&0!==d)return"boolean"===typeof d?d?1:-1:d||1;if("function"===typeof h.textSorter)return h.textSorter(b,a,c,f);c=b.replace(j[0], "\\0$1\\0").replace(/\\0$/,"").replace(/^\\0/,"").split("\\0");f=a.replace(j[0],"\\0$1\\0").replace(/\\0$/,"").replace(/^\\0/,"").split("\\0");b=parseInt(b.match(j[2]),16)||1!==c.length&&b.match(j[1])&&Date.parse(b);if(a=parseInt(a.match(j[2]),16)||b&&a.match(j[1])&&Date.parse(a)||null){if(ba)return 1}h=Math.max(c.length,f.length);for(b=0;bj)return 1}return 0};e.sortTextDesc=function(c,b,a,f){if(b===a)return 0;var d=c.config,g=d.string[d.empties[f]||d.emptyTo];return""===b&&0!==g?"boolean"===typeof g?g?-1:1:g||1:""===a&&0!==g?"boolean"===typeof g?g?1:-1:-g||-1:"function"===typeof d.textSorter?d.textSorter(a,b,c,f):e.sortText(c,a,b)};e.getTextValue=function(c,b,a){if(b){var f=c.length,d=b+a;for(b=0;bg.inArray(j[f].id,k)))h.debug&&d("Refeshing widgets: Removing "+j[f].id),j[f].hasOwnProperty("remove")&&j[f].remove(c,h,h.widgetOptions);!0!==a&&e.applyWidget(c,b)};e.getData=function(c,b,a){var d="";c=g(c);var e,k;if(!c.length)return"";e=g.metadata?c.metadata():!1;k=" "+(c.attr("class")||"");"undefined"!==typeof c.data(a)||"undefined"!==typeof c.data(a.toLowerCase())?d+=c.data(a)||c.data(a.toLowerCase()):e&&"undefined"!==typeof e[a]?d+=e[a]:b&&"undefined"!== typeof b[a]?d+=b[a]:" "!==k&&k.match(" "+a+"-")&&(d=k.match(RegExp(" "+a+"-(\\w+)"))[1]||"");return g.trim(d)};e.formatFloat=function(c,b){if("string"!==typeof c||""===c)return c;var a;c=(b&&b.config?!1!==b.config.usNumberFormat:"undefined"!==typeof b?b:1)?c.replace(/,/g,""):c.replace(/[\s|\.]/g,"").replace(/,/g,".");/^\s*\([.\d]+\)/.test(c)&&(c=c.replace(/^\s*\(/,"-").replace(/\)/,""));a=parseFloat(c);return isNaN(a)?g.trim(c):a};e.isDigit=function(c){return isNaN(c)?/^[\-+(]?\d+[)]?$/.test(c.toString().replace(/[,.'"\s]/g, "")):!0}}});var k=g.tablesorter;g.fn.extend({tablesorter:k.construct});k.addParser({id:"text",is:function(){return!0},format:function(d,v){var p=v.config;d=g.trim(p.ignoreCase?d.toLocaleLowerCase():d);return p.sortLocaleCompare?k.replaceAccents(d):d},type:"text"});k.addParser({id:"currency",is:function(d){return/^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/.test(d)},format:function(d,g){return k.formatFloat(d.replace(/[^\w,. \-()]/g,""),g)},type:"numeric"}); k.addParser({id:"ipAddress",is:function(d){return/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/.test(d)},format:function(d,g){var p,u=d.split("."),q="",t=u.length;for(p=0;p Date: Thu, 27 Dec 2012 16:37:49 -0600 Subject: Ensure mirror protocols are distinct Signed-off-by: Dan McGee --- mirrors/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mirrors/views.py b/mirrors/views.py index d0ce0a97..22da631a 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -161,7 +161,7 @@ def mirrors(request): mirror_list = Mirror.objects.select_related().order_by('tier', 'country') protos = MirrorUrl.objects.values_list( 'mirror_id', 'protocol__protocol').order_by( - 'mirror__id', 'protocol__protocol') + 'mirror__id', 'protocol__protocol').distinct() if not request.user.is_authenticated(): mirror_list = mirror_list.filter(public=True, active=True) protos = protos.filter(mirror__public=True, mirror__active=True) -- cgit v1.2.3-54-g00ecf From 2b9519996a47fd1d978ccac36246f0245ad668fb Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 27 Dec 2012 16:43:01 -0600 Subject: Update D3 to 3.0.0 Signed-off-by: Dan McGee --- templates/mirrors/mirror_details.html | 2 +- templates/public/keys.html | 2 +- templates/visualize/index.html | 2 +- visualize/static/d3-3.0.0.js | 7809 +++++++++++++++++++++++++++ visualize/static/d3-3.0.0.min.js | 4 + visualize/static/d3.v2.js | 9406 --------------------------------- visualize/static/d3.v2.min.js | 4 - 7 files changed, 7816 insertions(+), 9413 deletions(-) create mode 100644 visualize/static/d3-3.0.0.js create mode 100644 visualize/static/d3-3.0.0.min.js delete mode 100644 visualize/static/d3.v2.js delete mode 100644 visualize/static/d3.v2.min.js diff --git a/templates/mirrors/mirror_details.html b/templates/mirrors/mirror_details.html index 884187b9..132557cd 100644 --- a/templates/mirrors/mirror_details.html +++ b/templates/mirrors/mirror_details.html @@ -109,7 +109,7 @@

        Mirror Status Chart

        {% load cdn %}{% jquery %}{% jquery_tablesorter %} - + + + {% endblock %} diff --git a/templates/mirrors/status_table.html b/templates/mirrors/status_table.html index 1961d222..c7394de6 100644 --- a/templates/mirrors/status_table.html +++ b/templates/mirrors/status_table.html @@ -17,7 +17,7 @@ {% spaceless %} {{ m_url.url }} {{ m_url.protocol }} - {% if m_url.real_country %} {% endif %}{{ m_url.real_country.name }} + {% if m_url.country %} {% endif %}{{ m_url.country.name }} {{ m_url.completion_pct|percentage:1 }} {{ m_url.delay|duration|default:'unknown' }} {{ m_url.duration_avg|floatformat:2 }} diff --git a/templates/public/download.html b/templates/public/download.html index 3005ffb3..0c96fcef 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -81,7 +81,7 @@

        Checksums

        {% cache 600 download-mirrors %}
        - {% regroup mirror_urls by real_country as grouped_urls %} + {% regroup mirror_urls by country as grouped_urls %} {% for country in grouped_urls %} {% if country.grouper %}
        {{ country.grouper.name }}
        {% else %}
        Worldwide
        {% endif %} -- cgit v1.2.3-54-g00ecf From ff6db38f1dc6ed1eb53454a7e16615ec1ad76d7a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 14 Jan 2013 01:27:34 -0600 Subject: Ensure URLs without check data work on mirror details page Less noticeable in production as the templates don't show '@@@INVALID@@@' there, but we were trying to access attributes that don't actually exist on certain mirror objects. Signed-off-by: Dan McGee --- mirrors/views.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/mirrors/views.py b/mirrors/views.py index 545e3557..30df5472 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -176,13 +176,17 @@ def mirror_details(request, name): raise Http404 status_info = get_mirror_statuses(mirror_ids=[mirror.id]) - 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=attrgetter('url')) + checked_urls = {url for url in status_info['urls'] \ + if url.mirror_id == mirror.id} + all_urls = set(mirror.urls.select_related('protocol')) + # Add dummy data for URLs that we haven't checked recently + other_urls = all_urls.difference(checked_urls) + print other_urls + for url in other_urls: + for attr in ('last_sync', 'completion_pct', 'delay', 'duration_avg', + 'duration_stddev', 'score'): + setattr(url, attr, None) + all_urls = sorted(checked_urls.union(other_urls), key=attrgetter('url')) return render(request, 'mirrors/mirror_details.html', {'mirror': mirror, 'urls': all_urls}) -- cgit v1.2.3-54-g00ecf From 0f6a0a1cd0011c8ad137a4b27d0b39a7e1129fb7 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 14 Jan 2013 08:54:33 -0600 Subject: Support mirror status JSON by tier Just as we do for the normal status HTML view. Signed-off-by: Dan McGee --- mirrors/urls.py | 1 + mirrors/views.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/mirrors/urls.py b/mirrors/urls.py index 857e99e2..4e929410 100644 --- a/mirrors/urls.py +++ b/mirrors/urls.py @@ -5,6 +5,7 @@ (r'^status/$', 'status', {}, 'mirror-status'), (r'^status/json/$', 'status_json', {}, 'mirror-status-json'), (r'^status/tier/(?P\d+)/$', 'status', {}, 'mirror-status-tier'), + (r'^status/tier/(?P\d+)/json/$', 'status_json', {}, 'mirror-status-tier-json'), (r'^(?P[\.\-\w]+)/$', 'mirror_details'), (r'^(?P[\.\-\w]+)/json/$', 'mirror_details_json'), ) diff --git a/mirrors/views.py b/mirrors/views.py index 30df5472..c0ed6670 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -279,9 +279,15 @@ def default(self, obj): return super(ExtendedMirrorStatusJSONEncoder, self).default(obj) -def status_json(request): +def status_json(request, tier=None): + if tier is not None: + tier = int(tier) + if tier not in [t[0] for t in Mirror.TIER_CHOICES]: + raise Http404 status_info = get_mirror_statuses() data = status_info.copy() + if tier is not None: + data['urls'] = [url for url in data['urls'] if url.mirror.tier == tier] data['version'] = 3 to_json = json.dumps(data, ensure_ascii=False, cls=MirrorStatusJSONEncoder) response = HttpResponse(to_json, content_type='application/json') -- cgit v1.2.3-54-g00ecf From af32c23768c7537f19e0613525579208b4f44eb4 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 15 Jan 2013 20:49:56 -0600 Subject: Handle connection and transaction more properly in reporead A few minor things are fixed here. One is PostgreSQL, and more specifically pgbouncer, don't like it when the connection is closed after psycopg2 has started an implicit transaction even for read-only queries. Ensure we call commit as our last database action in all cases. The other is related- Django in management commands doesn't ever call close on any database connection you may have been using, so PostgreSQL gets mad about this fact and logs a message saying such. Close the connection explicitly when we are done with it to play nice. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 1 + devel/management/commands/reporead_inotify.py | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index e00e54c3..ab0efeed 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -589,6 +589,7 @@ def read_repo(primary_arch, repo_file, options): else: db_update(arch, repo, packages_arches[arch], force) logger.info('Finished database updates for %s.', repo_file) + connection.commit() connection.close() return 0 diff --git a/devel/management/commands/reporead_inotify.py b/devel/management/commands/reporead_inotify.py index 04f65764..8c1e47bf 100644 --- a/devel/management/commands/reporead_inotify.py +++ b/devel/management/commands/reporead_inotify.py @@ -23,7 +23,7 @@ import time from django.core.management.base import BaseCommand, CommandError -from django.db import connection +from django.db import connection, transaction from main.models import Arch, Repo from .reporead import read_repo @@ -53,6 +53,11 @@ def handle(self, path_template=None, **options): self.path_template = path_template notifier = self.setup_notifier() + # this thread is done using the database; all future access is done in + # the spawned read_repo() processes, so close the otherwise completely + # idle connection. + connection.close() + logger.info('Entering notifier loop') notifier.loop() @@ -61,14 +66,17 @@ def handle(self, path_template=None, **options): if hasattr(thread, 'cancel'): thread.cancel() + @transaction.commit_on_success def setup_notifier(self): '''Set up and configure the inotify machinery and logic. This takes the provided or default path_template and builds a list of directories we need to watch for database updates. It then validates and passes these on to the various pyinotify pieces as necessary and finally builds and returns a notifier object.''' + transaction.commit_manually() arches = Arch.objects.filter(agnostic=False) repos = Repo.objects.all() + transaction.set_dirty() arch_path_map = {arch: None for arch in arches} all_paths = set() total_paths = 0 @@ -91,11 +99,6 @@ def setup_notifier(self): raise CommandError('path template did not uniquely ' 'determine architecture for each file') - # this thread is done using the database; all future access is done in - # the spawned read_repo() processes, so close the otherwise completely - # idle connection. - connection.close() - # A proper atomic replacement of the database as done by rsync is type # IN_MOVED_TO. repo-add/remove will finish with a IN_CLOSE_WRITE. mask = pyinotify.IN_CLOSE_WRITE | pyinotify.IN_MOVED_TO -- cgit v1.2.3-54-g00ecf From 3a6398f42d04ea6a677bf7b6d5115175e9011432 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 15 Jan 2013 21:27:37 -0600 Subject: Add new AlwaysCommitMiddleware to the stack The reason for this is documented in the middleware itself. Without this, pgbouncer is of little use to us since it has to throw away every connection we try to route through it because of unclean disconnects. In theory, with the switch to using pgbouncer for all WSGI originating connections and adding this middleware, we should see a notable decrease in connection time to the database. Signed-off-by: Dan McGee --- main/middleware.py | 40 ++++++++++++++++++++++++++++++++++++++++ settings.py | 1 + 2 files changed, 41 insertions(+) create mode 100644 main/middleware.py diff --git a/main/middleware.py b/main/middleware.py new file mode 100644 index 00000000..a698b13c --- /dev/null +++ b/main/middleware.py @@ -0,0 +1,40 @@ +from django.core.exceptions import MiddlewareNotUsed +from django.db import connections + + +class AlwaysCommitMiddleware(object): + """ + Ensure we always commit any possibly open transaction so we leave the + database in a clean state. Without this, pgbouncer et al. always gives + error messages like this for every single request: + + LOG S-0x1accfd0: db/user@unix:5432 new connection to server + LOG C-0x1aaf620: db/user@unix:6432 closing because: client close request (age=0) + LOG S-0x1accfd0: db/user@unix:5432 closing because: unclean server (age=0) + + We only let this middleware apply for PostgreSQL backends; other databases + don't really require connection pooling and thus the reason for this + middleware's use is non-existent. + + The best location of this in your middleware stack is likely the top, as + you want to ensure it happens after any and all database activity has + completed. + """ + def __init__(self): + for conn in connections.all(): + if conn.vendor == 'postgresql': + return + raise MiddlewareNotUsed() + + def process_response(self, request, response): + """Commits any potentially open transactions at the underlying + PostgreSQL database connection level.""" + for conn in connections.all(): + if conn.vendor != 'postgresql': + continue + db_conn = getattr(conn, 'connection', None) + if db_conn is not None: + db_conn.commit() + return response + +# vim: set ts=4 sw=4 et: diff --git a/settings.py b/settings.py index 8ed5cb61..cdc56e3e 100644 --- a/settings.py +++ b/settings.py @@ -66,6 +66,7 @@ ) MIDDLEWARE_CLASSES = ( + 'main.middleware.AlwaysCommitMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', -- cgit v1.2.3-54-g00ecf From f106379b5382b2b82aa56466c8d3acaae58327a9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 15 Jan 2013 21:29:30 -0600 Subject: Clean up and make several migrations modern This moves most migrations to the v2 format that have been presenting some issues. One missing depends_on relationship has been added, and we allow an index to not be dropped if it does not exist due to the shittyness in sqlite3 actually keeping indexes across DDL on that table. Signed-off-by: Dan McGee --- main/migrations/0002_make_maintainer_nullable.py | 23 +++++++--------------- main/migrations/0003_migrate_maintainer.py | 8 +++----- main/migrations/0005_fix_empty_url_pkgdesc.py | 10 +++------- main/migrations/0013_mark_repos_testing.py | 6 +++--- main/migrations/0055_unique_package_in_repo.py | 7 ++++++- .../0002_add_todolist_and_todolistpackage.py | 4 ++++ 6 files changed, 26 insertions(+), 32 deletions(-) diff --git a/main/migrations/0002_make_maintainer_nullable.py b/main/migrations/0002_make_maintainer_nullable.py index 138b103b..675635df 100644 --- a/main/migrations/0002_make_maintainer_nullable.py +++ b/main/migrations/0002_make_maintainer_nullable.py @@ -1,26 +1,17 @@ - +# encoding: utf-8 from south.db import db +from south.v2 import SchemaMigration from django.db import models -from main.models import * -class Migration: - +class Migration(SchemaMigration): + def forwards(self, orm): - - # Changing field 'Package.maintainer' - # (to signature: django.db.models.fields.related.ForeignKey(null=True, to=orm['auth.User'])) db.alter_column('packages', 'maintainer_id', orm['main.package:maintainer']) - - - + def backwards(self, orm): - - # Changing field 'Package.maintainer' - # (to signature: django.db.models.fields.related.ForeignKey(to=orm['auth.User'])) db.alter_column('packages', 'maintainer_id', orm['main.package:maintainer']) - - - + + models = { 'auth.group': { 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), diff --git a/main/migrations/0003_migrate_maintainer.py b/main/migrations/0003_migrate_maintainer.py index a3a4793f..4169a5c9 100644 --- a/main/migrations/0003_migrate_maintainer.py +++ b/main/migrations/0003_migrate_maintainer.py @@ -1,11 +1,9 @@ - +# -*- coding: utf-8 -*- from south.db import db +from south.v2 import DataMigration from django.db import models -from main.models import * - -class Migration: - no_dry_run = True +class Migration(DataMigration): def forwards(self, orm): orm.Package.objects.filter(maintainer=0).update(maintainer=None) diff --git a/main/migrations/0005_fix_empty_url_pkgdesc.py b/main/migrations/0005_fix_empty_url_pkgdesc.py index c7cc1d8c..54658c17 100644 --- a/main/migrations/0005_fix_empty_url_pkgdesc.py +++ b/main/migrations/0005_fix_empty_url_pkgdesc.py @@ -1,14 +1,11 @@ - +# -*- coding: utf-8 -*- from south.db import db +from south.v2 import DataMigration from django.db import models -from main.models import * -class Migration: +class Migration(DataMigration): - no_dry_run = True - def forwards(self, orm): - "Write your forwards migration here" for p in orm.Package.objects.filter(pkgdesc=''): p.pkgdesc = None p.save() @@ -24,7 +21,6 @@ def forwards(self, orm): def backwards(self, orm): - "Write your backwards migration here" for p in orm.Package.objects.filter(pkgdesc=None): p.pkgdesc = '' p.save() diff --git a/main/migrations/0013_mark_repos_testing.py b/main/migrations/0013_mark_repos_testing.py index 617a3ab8..e50010b2 100644 --- a/main/migrations/0013_mark_repos_testing.py +++ b/main/migrations/0013_mark_repos_testing.py @@ -1,9 +1,9 @@ +# -*- coding: utf-8 -*- from south.db import db +from south.v2 import DataMigration from django.db import models -from main.models import * -class Migration: - no_dry_run = True +class Migration(DataMigration): def forwards(self, orm): orm.Repo.objects.filter(name__endswith="Testing").update(testing=True) diff --git a/main/migrations/0055_unique_package_in_repo.py b/main/migrations/0055_unique_package_in_repo.py index 36cc7193..9ae33719 100644 --- a/main/migrations/0055_unique_package_in_repo.py +++ b/main/migrations/0055_unique_package_in_repo.py @@ -2,11 +2,16 @@ from south.db import db from south.v2 import SchemaMigration from django.db import models +from django.db.utils import DatabaseError class Migration(SchemaMigration): def forwards(self, orm): - db.delete_index('packages', ['pkgname']) + try: + db.delete_index('packages', ['pkgname']) + except DatabaseError as e: + if not 'no such index' in str(e): + raise e db.create_unique('packages', ['pkgname', 'repo_id', 'arch_id']) def backwards(self, orm): diff --git a/todolists/migrations/0002_add_todolist_and_todolistpackage.py b/todolists/migrations/0002_add_todolist_and_todolistpackage.py index 8365535a..ba8f7ebe 100644 --- a/todolists/migrations/0002_add_todolist_and_todolistpackage.py +++ b/todolists/migrations/0002_add_todolist_and_todolistpackage.py @@ -6,6 +6,10 @@ class Migration(SchemaMigration): + depends_on = ( + ('main', '0024_set_initial_flag_date'), + ) + def forwards(self, orm): db.create_table('todolists_todolist', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), -- cgit v1.2.3-54-g00ecf From 8eaa63b2976e697cd2e6adca43f5d6d9cf7a8eda Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 15 Jan 2013 23:06:08 -0600 Subject: Tabs -> spaces in archweb.css Signed-off-by: Dan McGee --- sitestatic/archweb.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index cfa30f5e..149f2b4c 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -403,7 +403,7 @@ ul.errorlist { /* JS sorting via tablesorter */ table th.tablesorter-header { padding-right: 20px; - background-image: url(data:image/gif;base64,R0lGODlhFQAJAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAkAAAIXjI+AywnaYnhUMoqt3gZXPmVg94yJVQAAOw==); + background-image: url(data:image/gif;base64,R0lGODlhFQAJAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAkAAAIXjI+AywnaYnhUMoqt3gZXPmVg94yJVQAAOw==); background-repeat: no-repeat; background-position: center right; cursor: pointer; @@ -411,12 +411,12 @@ table th.tablesorter-header { table thead th.tablesorter-headerAsc { background-color: #e4eeff; - background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7); + background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7); } table thead th.tablesorter-headerDesc { background-color: #e4eeff; - background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7); + background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7); } table thead th.sorter-false { -- cgit v1.2.3-54-g00ecf From 131d238ae38034c3df0ab1dbc307773ac6a38442 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 15 Jan 2013 22:59:13 -0600 Subject: Add a little easter egg for people to find Signed-off-by: Dan McGee --- sitestatic/archweb.css | 11 +++++++++++ sitestatic/konami.pack.js | 1 + sitestatic/vector_tux.png | Bin 0 -> 165926 bytes templates/base.html | 1 + templates/public/index.html | 16 ++++++++++++++++ 5 files changed, 29 insertions(+) create mode 100644 sitestatic/konami.pack.js create mode 100644 sitestatic/vector_tux.png diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 149f2b4c..f43bba1f 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -599,6 +599,17 @@ div.widget { margin-bottom: 1.5em; } +/* home: other stuff */ +#konami { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; + opacity: 0.6; +} + /* feeds page */ #rss-feeds .rss { padding-right: 20px; diff --git a/sitestatic/konami.pack.js b/sitestatic/konami.pack.js new file mode 100644 index 00000000..bff279f9 --- /dev/null +++ b/sitestatic/konami.pack.js @@ -0,0 +1 @@ +(function(){"use strict";var a=Function("return this")(),b=function(){var a={addEvent:function(a,b,c,d){a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent&&(a["e"+b+c]=c,a[b+c]=function(){a["e"+b+c](window.event,d)},a.attachEvent("on"+b,a[b+c]))},input:"",pattern:"3838404037393739666513",load:function(b){this.addEvent(document,"keydown",function(c,d){return d&&(a=d),a.input+=c?c.keyCode:event.keyCode,a.input.length>a.pattern.length&&(a.input=a.input.substr(a.input.length-a.pattern.length)),a.input==a.pattern?(a.code(b),a.input="",void 0):void 0},this),this.iphone.load(b)},code:function(a){window.location=a},iphone:{start_x:0,start_y:0,stop_x:0,stop_y:0,tap:!1,capture:!1,orig_keys:"",keys:["UP","UP","DOWN","DOWN","LEFT","RIGHT","LEFT","RIGHT","TAP","TAP","TAP"],code:function(b){a.code(b)},load:function(b){this.orig_keys=this.keys,a.addEvent(document,"touchmove",function(b){if(1==b.touches.length&&1==a.iphone.capture){var c=b.touches[0];a.iphone.stop_x=c.pageX,a.iphone.stop_y=c.pageY,a.iphone.tap=!1,a.iphone.capture=!1,a.iphone.check_direction()}}),a.addEvent(document,"touchend",function(){1==a.iphone.tap&&a.iphone.check_direction(b)},!1),a.addEvent(document,"touchstart",function(b){a.iphone.start_x=b.changedTouches[0].pageX,a.iphone.start_y=b.changedTouches[0].pageY,a.iphone.tap=!0,a.iphone.capture=!0})},check_direction:function(a){x_magnitude=Math.abs(this.start_x-this.stop_x),y_magnitude=Math.abs(this.start_y-this.stop_y),x=0>this.start_x-this.stop_x?"RIGHT":"LEFT",y=0>this.start_y-this.stop_y?"DOWN":"UP",result=x_magnitude>y_magnitude?x:y,result=1==this.tap?"TAP":result,result==this.keys[0]&&(this.keys=this.keys.slice(1,this.keys.length)),0==this.keys.length&&(this.keys=this.orig_keys,this.code(a))}}};return a};"undefined"!=typeof module?module.exports=b:a.Konami=b})(); diff --git a/sitestatic/vector_tux.png b/sitestatic/vector_tux.png new file mode 100644 index 00000000..ab4be6d0 Binary files /dev/null and b/sitestatic/vector_tux.png differ diff --git a/templates/base.html b/templates/base.html index a7ebc7d3..c6aa3f17 100644 --- a/templates/base.html +++ b/templates/base.html @@ -68,6 +68,7 @@ {% block content_right %}{% endblock %}
        {% endblock %} + {% block content_after %}{% endblock %}

    {% endcache %} +{% endblock %} + +{% block content_after %} + {% load cdn %}{% jquery %} + {% endblock %} -- cgit v1.2.3-54-g00ecf From 5ddf48cf741dfca4e15964aa15f87aab9ac81028 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 31 Dec 2012 14:02:57 -0600 Subject: Add famfamfam flags sprite image and CSS Signed-off-by: Dan McGee --- sitestatic/famfamfam-flags.css | 275 +++++++++++++++++++++++++++++++++++++++++ sitestatic/famfamfam-flags.png | Bin 0 -> 76543 bytes 2 files changed, 275 insertions(+) create mode 100644 sitestatic/famfamfam-flags.css create mode 100644 sitestatic/famfamfam-flags.png diff --git a/sitestatic/famfamfam-flags.css b/sitestatic/famfamfam-flags.css new file mode 100644 index 00000000..88c5612f --- /dev/null +++ b/sitestatic/famfamfam-flags.css @@ -0,0 +1,275 @@ +/** + * FAMFAMFAM flag icons CSS. + * + * Examples: + * France + * United States + */ + +[class^="famfamfam-flag"] { + display: inline-block; + width: 16px; + height: 11px; + line-height: 11px; + /* vertical-align: text-top; */ + background-image: url("famfamfam-flags.png"); + background-position: 0 0; + background-repeat: no-repeat; +} + +.famfamfam-flag-zw { background-position: 0px 0px; width: 16px; height: 11px; } +.famfamfam-flag-zm { background-position: -16px 0px; width: 16px; height: 11px; } +.famfamfam-flag-za { background-position: 0px -11px; width: 16px; height: 11px; } +.famfamfam-flag-yt { background-position: -16px -11px; width: 16px; height: 11px; } +.famfamfam-flag-ye { background-position: -32px 0px; width: 16px; height: 11px; } +.famfamfam-flag-ws { background-position: -32px -11px; width: 16px; height: 11px; } +.famfamfam-flag-wf { background-position: 0px -22px; width: 16px; height: 11px; } +.famfamfam-flag-wales { background-position: -16px -22px; width: 16px; height: 11px; } +.famfamfam-flag-vu { background-position: -32px -22px; width: 16px; height: 11px; } +.famfamfam-flag-vn { background-position: 0px -33px; width: 16px; height: 11px; } +.famfamfam-flag-vi { background-position: -16px -33px; width: 16px; height: 11px; } +.famfamfam-flag-vg { background-position: -32px -33px; width: 16px; height: 11px; } +.famfamfam-flag-ve { background-position: -48px 0px; width: 16px; height: 11px; } +.famfamfam-flag-vc { background-position: -48px -11px; width: 16px; height: 11px; } +.famfamfam-flag-va { background-position: -48px -22px; width: 16px; height: 11px; } +.famfamfam-flag-uz { background-position: -48px -33px; width: 16px; height: 11px; } +.famfamfam-flag-uy { background-position: 0px -44px; width: 16px; height: 11px; } +.famfamfam-flag-us { background-position: -16px -44px; width: 16px; height: 11px; } +.famfamfam-flag-um { background-position: -16px -44px; width: 16px; height: 11px; } +.famfamfam-flag-ug { background-position: -32px -44px; width: 16px; height: 11px; } +.famfamfam-flag-ua { background-position: -48px -44px; width: 16px; height: 11px; } +.famfamfam-flag-tz { background-position: -64px 0px; width: 16px; height: 11px; } +.famfamfam-flag-tw { background-position: -64px -11px; width: 16px; height: 11px; } +.famfamfam-flag-tv { background-position: -64px -22px; width: 16px; height: 11px; } +.famfamfam-flag-tt { background-position: -64px -33px; width: 16px; height: 11px; } +.famfamfam-flag-tr { background-position: -64px -44px; width: 16px; height: 11px; } +.famfamfam-flag-to { background-position: 0px -55px; width: 16px; height: 11px; } +.famfamfam-flag-tn { background-position: -16px -55px; width: 16px; height: 11px; } +.famfamfam-flag-tm { background-position: -32px -55px; width: 16px; height: 11px; } +.famfamfam-flag-tl { background-position: -48px -55px; width: 16px; height: 11px; } +.famfamfam-flag-tk { background-position: -64px -55px; width: 16px; height: 11px; } +.famfamfam-flag-tj { background-position: 0px -66px; width: 16px; height: 11px; } +.famfamfam-flag-th { background-position: -16px -66px; width: 16px; height: 11px; } +.famfamfam-flag-tg { background-position: -32px -66px; width: 16px; height: 11px; } +.famfamfam-flag-tf { background-position: -48px -66px; width: 16px; height: 11px; } +.famfamfam-flag-td { background-position: -64px -66px; width: 16px; height: 11px; } +.famfamfam-flag-tc { background-position: -80px 0px; width: 16px; height: 11px; } +.famfamfam-flag-sz { background-position: -80px -11px; width: 16px; height: 11px; } +.famfamfam-flag-sy { background-position: -80px -22px; width: 16px; height: 11px; } +.famfamfam-flag-sx { background-position: -80px -33px; width: 16px; height: 11px; } +.famfamfam-flag-sv { background-position: -80px -44px; width: 16px; height: 11px; } +.famfamfam-flag-st { background-position: -80px -55px; width: 16px; height: 11px; } +.famfamfam-flag-ss { background-position: -80px -66px; width: 16px; height: 11px; } +.famfamfam-flag-sr { background-position: 0px -77px; width: 16px; height: 11px; } +.famfamfam-flag-so { background-position: -16px -77px; width: 16px; height: 11px; } +.famfamfam-flag-sn { background-position: -32px -77px; width: 16px; height: 11px; } +.famfamfam-flag-sm { background-position: -48px -77px; width: 16px; height: 11px; } +.famfamfam-flag-sl { background-position: -64px -77px; width: 16px; height: 11px; } +.famfamfam-flag-sk { background-position: -80px -77px; width: 16px; height: 11px; } +.famfamfam-flag-si { background-position: -96px 0px; width: 16px; height: 11px; } +.famfamfam-flag-sh { background-position: -96px -11px; width: 16px; height: 11px; } +.famfamfam-flag-sg { background-position: -96px -22px; width: 16px; height: 11px; } +.famfamfam-flag-se { background-position: -96px -33px; width: 16px; height: 11px; } +.famfamfam-flag-sd { background-position: -96px -44px; width: 16px; height: 11px; } +.famfamfam-flag-scotland { background-position: -96px -55px; width: 16px; height: 11px; } +.famfamfam-flag-sc { background-position: -96px -66px; width: 16px; height: 11px; } +.famfamfam-flag-sb { background-position: -96px -77px; width: 16px; height: 11px; } +.famfamfam-flag-sa { background-position: 0px -88px; width: 16px; height: 11px; } +.famfamfam-flag-rw { background-position: -16px -88px; width: 16px; height: 11px; } +.famfamfam-flag-ru { background-position: -32px -88px; width: 16px; height: 11px; } +.famfamfam-flag-rs { background-position: -48px -88px; width: 16px; height: 11px; } +.famfamfam-flag-ro { background-position: -64px -88px; width: 16px; height: 11px; } +.famfamfam-flag-qa { background-position: -80px -88px; width: 16px; height: 11px; } +.famfamfam-flag-py { background-position: -96px -88px; width: 16px; height: 11px; } +.famfamfam-flag-pw { background-position: 0px -99px; width: 16px; height: 11px; } +.famfamfam-flag-pt { background-position: -16px -99px; width: 16px; height: 11px; } +.famfamfam-flag-ps { background-position: -32px -99px; width: 16px; height: 11px; } +.famfamfam-flag-pr { background-position: -48px -99px; width: 16px; height: 11px; } +.famfamfam-flag-pn { background-position: -64px -99px; width: 16px; height: 11px; } +.famfamfam-flag-pm { background-position: -80px -99px; width: 16px; height: 11px; } +.famfamfam-flag-pl { background-position: -96px -99px; width: 16px; height: 11px; } +.famfamfam-flag-pk { background-position: -112px 0px; width: 16px; height: 11px; } +.famfamfam-flag-ph { background-position: -112px -11px; width: 16px; height: 11px; } +.famfamfam-flag-pg { background-position: -112px -22px; width: 16px; height: 11px; } +.famfamfam-flag-pf { background-position: -112px -33px; width: 16px; height: 11px; } +.famfamfam-flag-pe { background-position: -112px -44px; width: 16px; height: 11px; } +.famfamfam-flag-pa { background-position: -112px -55px; width: 16px; height: 11px; } +.famfamfam-flag-om { background-position: -112px -66px; width: 16px; height: 11px; } +.famfamfam-flag-nz { background-position: -112px -77px; width: 16px; height: 11px; } +.famfamfam-flag-nu { background-position: -112px -88px; width: 16px; height: 11px; } +.famfamfam-flag-nr { background-position: -112px -99px; width: 16px; height: 11px; } +.famfamfam-flag-no { background-position: 0px -110px; width: 16px; height: 11px; } +.famfamfam-flag-bv { background-position: 0px -110px; width: 16px; height: 11px; } +.famfamfam-flag-sj { background-position: 0px -110px; width: 16px; height: 11px; } +.famfamfam-flag-nl { background-position: -16px -110px; width: 16px; height: 11px; } +.famfamfam-flag-ni { background-position: -32px -110px; width: 16px; height: 11px; } +.famfamfam-flag-ng { background-position: -48px -110px; width: 16px; height: 11px; } +.famfamfam-flag-nf { background-position: -64px -110px; width: 16px; height: 11px; } +.famfamfam-flag-ne { background-position: -80px -110px; width: 16px; height: 11px; } +.famfamfam-flag-nc { background-position: -96px -110px; width: 16px; height: 11px; } +.famfamfam-flag-na { background-position: -112px -110px; width: 16px; height: 11px; } +.famfamfam-flag-mz { background-position: -128px 0px; width: 16px; height: 11px; } +.famfamfam-flag-my { background-position: -128px -11px; width: 16px; height: 11px; } +.famfamfam-flag-mx { background-position: -128px -22px; width: 16px; height: 11px; } +.famfamfam-flag-mw { background-position: -128px -33px; width: 16px; height: 11px; } +.famfamfam-flag-mv { background-position: -128px -44px; width: 16px; height: 11px; } +.famfamfam-flag-mu { background-position: -128px -55px; width: 16px; height: 11px; } +.famfamfam-flag-mt { background-position: -128px -66px; width: 16px; height: 11px; } +.famfamfam-flag-ms { background-position: -128px -77px; width: 16px; height: 11px; } +.famfamfam-flag-mr { background-position: -128px -88px; width: 16px; height: 11px; } +.famfamfam-flag-mq { background-position: -128px -99px; width: 16px; height: 11px; } +.famfamfam-flag-mp { background-position: -128px -110px; width: 16px; height: 11px; } +.famfamfam-flag-mo { background-position: 0px -121px; width: 16px; height: 11px; } +.famfamfam-flag-mn { background-position: -16px -121px; width: 16px; height: 11px; } +.famfamfam-flag-mm { background-position: -32px -121px; width: 16px; height: 11px; } +.famfamfam-flag-ml { background-position: -48px -121px; width: 16px; height: 11px; } +.famfamfam-flag-mk { background-position: -64px -121px; width: 16px; height: 11px; } +.famfamfam-flag-mh { background-position: -80px -121px; width: 16px; height: 11px; } +.famfamfam-flag-mg { background-position: -96px -121px; width: 16px; height: 11px; } +.famfamfam-flag-me { background-position: 0px -132px; width: 16px; height: 12px; } +.famfamfam-flag-md { background-position: -112px -121px; width: 16px; height: 11px; } +.famfamfam-flag-mc { background-position: -128px -121px; width: 16px; height: 11px; } +.famfamfam-flag-ma { background-position: -16px -132px; width: 16px; height: 11px; } +.famfamfam-flag-ly { background-position: -32px -132px; width: 16px; height: 11px; } +.famfamfam-flag-lv { background-position: -48px -132px; width: 16px; height: 11px; } +.famfamfam-flag-lu { background-position: -64px -132px; width: 16px; height: 11px; } +.famfamfam-flag-lt { background-position: -80px -132px; width: 16px; height: 11px; } +.famfamfam-flag-ls { background-position: -96px -132px; width: 16px; height: 11px; } +.famfamfam-flag-lr { background-position: -112px -132px; width: 16px; height: 11px; } +.famfamfam-flag-lk { background-position: -128px -132px; width: 16px; height: 11px; } +.famfamfam-flag-li { background-position: -144px 0px; width: 16px; height: 11px; } +.famfamfam-flag-lc { background-position: -144px -11px; width: 16px; height: 11px; } +.famfamfam-flag-lb { background-position: -144px -22px; width: 16px; height: 11px; } +.famfamfam-flag-la { background-position: -144px -33px; width: 16px; height: 11px; } +.famfamfam-flag-kz { background-position: -144px -44px; width: 16px; height: 11px; } +.famfamfam-flag-ky { background-position: -144px -55px; width: 16px; height: 11px; } +.famfamfam-flag-kw { background-position: -144px -66px; width: 16px; height: 11px; } +.famfamfam-flag-kr { background-position: -144px -77px; width: 16px; height: 11px; } +.famfamfam-flag-kp { background-position: -144px -88px; width: 16px; height: 11px; } +.famfamfam-flag-kn { background-position: -144px -99px; width: 16px; height: 11px; } +.famfamfam-flag-km { background-position: -144px -110px; width: 16px; height: 11px; } +.famfamfam-flag-ki { background-position: -144px -121px; width: 16px; height: 11px; } +.famfamfam-flag-kh { background-position: -144px -132px; width: 16px; height: 11px; } +.famfamfam-flag-kg { background-position: 0px -144px; width: 16px; height: 11px; } +.famfamfam-flag-ke { background-position: -16px -144px; width: 16px; height: 11px; } +.famfamfam-flag-jp { background-position: -32px -144px; width: 16px; height: 11px; } +.famfamfam-flag-jo { background-position: -48px -144px; width: 16px; height: 11px; } +.famfamfam-flag-jm { background-position: -64px -144px; width: 16px; height: 11px; } +.famfamfam-flag-je { background-position: -80px -144px; width: 16px; height: 11px; } +.famfamfam-flag-it { background-position: -96px -144px; width: 16px; height: 11px; } +.famfamfam-flag-is { background-position: -112px -144px; width: 16px; height: 11px; } +.famfamfam-flag-ir { background-position: -128px -144px; width: 16px; height: 11px; } +.famfamfam-flag-iq { background-position: -144px -144px; width: 16px; height: 11px; } +.famfamfam-flag-io { background-position: -160px 0px; width: 16px; height: 11px; } +.famfamfam-flag-in { background-position: -160px -11px; width: 16px; height: 11px; } +.famfamfam-flag-im { background-position: -160px -22px; width: 16px; height: 9px; } +.famfamfam-flag-il { background-position: -160px -31px; width: 16px; height: 11px; } +.famfamfam-flag-ie { background-position: -160px -42px; width: 16px; height: 11px; } +.famfamfam-flag-id { background-position: -160px -53px; width: 16px; height: 11px; } +.famfamfam-flag-hu { background-position: -160px -64px; width: 16px; height: 11px; } +.famfamfam-flag-ht { background-position: -160px -75px; width: 16px; height: 11px; } +.famfamfam-flag-hr { background-position: -160px -86px; width: 16px; height: 11px; } +.famfamfam-flag-hn { background-position: -160px -97px; width: 16px; height: 11px; } +.famfamfam-flag-hk { background-position: -160px -108px; width: 16px; height: 11px; } +.famfamfam-flag-gy { background-position: -160px -119px; width: 16px; height: 11px; } +.famfamfam-flag-gw { background-position: -160px -130px; width: 16px; height: 11px; } +.famfamfam-flag-gu { background-position: -160px -141px; width: 16px; height: 11px; } +.famfamfam-flag-gt { background-position: 0px -155px; width: 16px; height: 11px; } +.famfamfam-flag-gs { background-position: -16px -155px; width: 16px; height: 11px; } +.famfamfam-flag-gr { background-position: -32px -155px; width: 16px; height: 11px; } +.famfamfam-flag-gq { background-position: -48px -155px; width: 16px; height: 11px; } +.famfamfam-flag-gp { background-position: -64px -155px; width: 16px; height: 11px; } +.famfamfam-flag-gn { background-position: -80px -155px; width: 16px; height: 11px; } +.famfamfam-flag-gm { background-position: -96px -155px; width: 16px; height: 11px; } +.famfamfam-flag-gl { background-position: -112px -155px; width: 16px; height: 11px; } +.famfamfam-flag-gi { background-position: -128px -155px; width: 16px; height: 11px; } +.famfamfam-flag-gh { background-position: -144px -155px; width: 16px; height: 11px; } +.famfamfam-flag-gg { background-position: -160px -155px; width: 16px; height: 11px; } +.famfamfam-flag-ge { background-position: -176px 0px; width: 16px; height: 11px; } +.famfamfam-flag-gd { background-position: -176px -11px; width: 16px; height: 11px; } +.famfamfam-flag-gb { background-position: -176px -22px; width: 16px; height: 11px; } +.famfamfam-flag-ga { background-position: -176px -33px; width: 16px; height: 11px; } +.famfamfam-flag-fr { background-position: -176px -44px; width: 16px; height: 11px; } +.famfamfam-flag-gf { background-position: -176px -44px; width: 16px; height: 11px; } +.famfamfam-flag-re { background-position: -176px -44px; width: 16px; height: 11px; } +.famfamfam-flag-mf { background-position: -176px -44px; width: 16px; height: 11px; } +.famfamfam-flag-bl { background-position: -176px -44px; width: 16px; height: 11px; } +.famfamfam-flag-fo { background-position: -176px -55px; width: 16px; height: 11px; } +.famfamfam-flag-fm { background-position: -176px -66px; width: 16px; height: 11px; } +.famfamfam-flag-fk { background-position: -176px -77px; width: 16px; height: 11px; } +.famfamfam-flag-fj { background-position: -176px -88px; width: 16px; height: 11px; } +.famfamfam-flag-fi { background-position: -176px -99px; width: 16px; height: 11px; } +.famfamfam-flag-fam { background-position: -176px -110px; width: 16px; height: 11px; } +.famfamfam-flag-eu { background-position: -176px -121px; width: 16px; height: 11px; } +.famfamfam-flag-et { background-position: -176px -132px; width: 16px; height: 11px; } +.famfamfam-flag-es { background-position: -176px -143px; width: 16px; height: 11px; } +.famfamfam-flag-er { background-position: -176px -154px; width: 16px; height: 11px; } +.famfamfam-flag-england { background-position: 0px -166px; width: 16px; height: 11px; } +.famfamfam-flag-eh { background-position: -16px -166px; width: 16px; height: 11px; } +.famfamfam-flag-eg { background-position: -32px -166px; width: 16px; height: 11px; } +.famfamfam-flag-ee { background-position: -48px -166px; width: 16px; height: 11px; } +.famfamfam-flag-ec { background-position: -64px -166px; width: 16px; height: 11px; } +.famfamfam-flag-dz { background-position: -80px -166px; width: 16px; height: 11px; } +.famfamfam-flag-do { background-position: -96px -166px; width: 16px; height: 11px; } +.famfamfam-flag-dm { background-position: -112px -166px; width: 16px; height: 11px; } +.famfamfam-flag-dk { background-position: -128px -166px; width: 16px; height: 11px; } +.famfamfam-flag-dj { background-position: -144px -166px; width: 16px; height: 11px; } +.famfamfam-flag-de { background-position: -160px -166px; width: 16px; height: 11px; } +.famfamfam-flag-cz { background-position: -176px -166px; width: 16px; height: 11px; } +.famfamfam-flag-cy { background-position: 0px -177px; width: 16px; height: 11px; } +.famfamfam-flag-cx { background-position: -16px -177px; width: 16px; height: 11px; } +.famfamfam-flag-cw { background-position: -32px -177px; width: 16px; height: 11px; } +.famfamfam-flag-cv { background-position: -48px -177px; width: 16px; height: 11px; } +.famfamfam-flag-cu { background-position: -64px -177px; width: 16px; height: 11px; } +.famfamfam-flag-cs { background-position: -80px -177px; width: 16px; height: 11px; } +.famfamfam-flag-cr { background-position: -96px -177px; width: 16px; height: 11px; } +.famfamfam-flag-co { background-position: -112px -177px; width: 16px; height: 11px; } +.famfamfam-flag-cn { background-position: -128px -177px; width: 16px; height: 11px; } +.famfamfam-flag-cm { background-position: -144px -177px; width: 16px; height: 11px; } +.famfamfam-flag-cl { background-position: -160px -177px; width: 16px; height: 11px; } +.famfamfam-flag-ck { background-position: -176px -177px; width: 16px; height: 11px; } +.famfamfam-flag-ci { background-position: -192px 0px; width: 16px; height: 11px; } +.famfamfam-flag-cg { background-position: -192px -11px; width: 16px; height: 11px; } +.famfamfam-flag-cf { background-position: -192px -22px; width: 16px; height: 11px; } +.famfamfam-flag-cd { background-position: -192px -33px; width: 16px; height: 11px; } +.famfamfam-flag-cc { background-position: -192px -44px; width: 16px; height: 11px; } +.famfamfam-flag-catalonia { background-position: -192px -55px; width: 16px; height: 11px; } +.famfamfam-flag-ca { background-position: -192px -66px; width: 16px; height: 11px; } +.famfamfam-flag-bz { background-position: -192px -77px; width: 16px; height: 11px; } +.famfamfam-flag-by { background-position: -192px -88px; width: 16px; height: 11px; } +.famfamfam-flag-bw { background-position: -192px -99px; width: 16px; height: 11px; } +.famfamfam-flag-bt { background-position: -192px -110px; width: 16px; height: 11px; } +.famfamfam-flag-bs { background-position: -192px -121px; width: 16px; height: 11px; } +.famfamfam-flag-br { background-position: -192px -132px; width: 16px; height: 11px; } +.famfamfam-flag-bq { background-position: -192px -143px; width: 16px; height: 11px; } +.famfamfam-flag-bo { background-position: -192px -154px; width: 16px; height: 11px; } +.famfamfam-flag-bn { background-position: -192px -165px; width: 16px; height: 11px; } +.famfamfam-flag-bm { background-position: -192px -176px; width: 16px; height: 11px; } +.famfamfam-flag-bj { background-position: 0px -188px; width: 16px; height: 11px; } +.famfamfam-flag-bi { background-position: -16px -188px; width: 16px; height: 11px; } +.famfamfam-flag-bh { background-position: -32px -188px; width: 16px; height: 11px; } +.famfamfam-flag-bg { background-position: -48px -188px; width: 16px; height: 11px; } +.famfamfam-flag-bf { background-position: -64px -188px; width: 16px; height: 11px; } +.famfamfam-flag-be { background-position: -80px -188px; width: 16px; height: 11px; } +.famfamfam-flag-bd { background-position: -96px -188px; width: 16px; height: 11px; } +.famfamfam-flag-bb { background-position: -112px -188px; width: 16px; height: 11px; } +.famfamfam-flag-ba { background-position: -128px -188px; width: 16px; height: 11px; } +.famfamfam-flag-az { background-position: -144px -188px; width: 16px; height: 11px; } +.famfamfam-flag-ax { background-position: -160px -188px; width: 16px; height: 11px; } +.famfamfam-flag-aw { background-position: -176px -188px; width: 16px; height: 11px; } +.famfamfam-flag-au { background-position: -192px -188px; width: 16px; height: 11px; } +.famfamfam-flag-hm { background-position: -192px -188px; width: 16px; height: 11px; } +.famfamfam-flag-at { background-position: -208px 0px; width: 16px; height: 11px; } +.famfamfam-flag-as { background-position: -208px -11px; width: 16px; height: 11px; } +.famfamfam-flag-ar { background-position: -208px -22px; width: 16px; height: 11px; } +.famfamfam-flag-ao { background-position: -208px -33px; width: 16px; height: 11px; } +.famfamfam-flag-an { background-position: -208px -44px; width: 16px; height: 11px; } +.famfamfam-flag-am { background-position: -208px -55px; width: 16px; height: 11px; } +.famfamfam-flag-al { background-position: -208px -66px; width: 16px; height: 11px; } +.famfamfam-flag-ai { background-position: -208px -77px; width: 16px; height: 11px; } +.famfamfam-flag-ag { background-position: -208px -88px; width: 16px; height: 11px; } +.famfamfam-flag-af { background-position: -208px -99px; width: 16px; height: 11px; } +.famfamfam-flag-ae { background-position: -208px -110px; width: 16px; height: 11px; } +.famfamfam-flag-ad { background-position: -208px -121px; width: 16px; height: 11px; } +.famfamfam-flag-np { background-position: -208px -132px; width: 9px; height: 11px; } +.famfamfam-flag-ch { background-position: -208px -143px; width: 11px; height: 11px; } diff --git a/sitestatic/famfamfam-flags.png b/sitestatic/famfamfam-flags.png new file mode 100644 index 00000000..953287b9 Binary files /dev/null and b/sitestatic/famfamfam-flags.png differ -- cgit v1.2.3-54-g00ecf From 0cb66a586bb9c70353b8388c8a448c1deff9e360 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 31 Dec 2012 14:06:36 -0600 Subject: Simplify famfamfam flags CSS Remove any redundant width and height declarations from individual countries and shorten the CSS class names. Signed-off-by: Dan McGee --- sitestatic/famfamfam-flags.css | 520 ++++++++++++++++++++--------------------- 1 file changed, 258 insertions(+), 262 deletions(-) diff --git a/sitestatic/famfamfam-flags.css b/sitestatic/famfamfam-flags.css index 88c5612f..b8e6ab5a 100644 --- a/sitestatic/famfamfam-flags.css +++ b/sitestatic/famfamfam-flags.css @@ -1,12 +1,8 @@ /** - * FAMFAMFAM flag icons CSS. - * - * Examples: - * France - * United States + * fam flag icons CSS. */ -[class^="famfamfam-flag"] { +[class^="fam-flag"] { display: inline-block; width: 16px; height: 11px; @@ -17,259 +13,259 @@ background-repeat: no-repeat; } -.famfamfam-flag-zw { background-position: 0px 0px; width: 16px; height: 11px; } -.famfamfam-flag-zm { background-position: -16px 0px; width: 16px; height: 11px; } -.famfamfam-flag-za { background-position: 0px -11px; width: 16px; height: 11px; } -.famfamfam-flag-yt { background-position: -16px -11px; width: 16px; height: 11px; } -.famfamfam-flag-ye { background-position: -32px 0px; width: 16px; height: 11px; } -.famfamfam-flag-ws { background-position: -32px -11px; width: 16px; height: 11px; } -.famfamfam-flag-wf { background-position: 0px -22px; width: 16px; height: 11px; } -.famfamfam-flag-wales { background-position: -16px -22px; width: 16px; height: 11px; } -.famfamfam-flag-vu { background-position: -32px -22px; width: 16px; height: 11px; } -.famfamfam-flag-vn { background-position: 0px -33px; width: 16px; height: 11px; } -.famfamfam-flag-vi { background-position: -16px -33px; width: 16px; height: 11px; } -.famfamfam-flag-vg { background-position: -32px -33px; width: 16px; height: 11px; } -.famfamfam-flag-ve { background-position: -48px 0px; width: 16px; height: 11px; } -.famfamfam-flag-vc { background-position: -48px -11px; width: 16px; height: 11px; } -.famfamfam-flag-va { background-position: -48px -22px; width: 16px; height: 11px; } -.famfamfam-flag-uz { background-position: -48px -33px; width: 16px; height: 11px; } -.famfamfam-flag-uy { background-position: 0px -44px; width: 16px; height: 11px; } -.famfamfam-flag-us { background-position: -16px -44px; width: 16px; height: 11px; } -.famfamfam-flag-um { background-position: -16px -44px; width: 16px; height: 11px; } -.famfamfam-flag-ug { background-position: -32px -44px; width: 16px; height: 11px; } -.famfamfam-flag-ua { background-position: -48px -44px; width: 16px; height: 11px; } -.famfamfam-flag-tz { background-position: -64px 0px; width: 16px; height: 11px; } -.famfamfam-flag-tw { background-position: -64px -11px; width: 16px; height: 11px; } -.famfamfam-flag-tv { background-position: -64px -22px; width: 16px; height: 11px; } -.famfamfam-flag-tt { background-position: -64px -33px; width: 16px; height: 11px; } -.famfamfam-flag-tr { background-position: -64px -44px; width: 16px; height: 11px; } -.famfamfam-flag-to { background-position: 0px -55px; width: 16px; height: 11px; } -.famfamfam-flag-tn { background-position: -16px -55px; width: 16px; height: 11px; } -.famfamfam-flag-tm { background-position: -32px -55px; width: 16px; height: 11px; } -.famfamfam-flag-tl { background-position: -48px -55px; width: 16px; height: 11px; } -.famfamfam-flag-tk { background-position: -64px -55px; width: 16px; height: 11px; } -.famfamfam-flag-tj { background-position: 0px -66px; width: 16px; height: 11px; } -.famfamfam-flag-th { background-position: -16px -66px; width: 16px; height: 11px; } -.famfamfam-flag-tg { background-position: -32px -66px; width: 16px; height: 11px; } -.famfamfam-flag-tf { background-position: -48px -66px; width: 16px; height: 11px; } -.famfamfam-flag-td { background-position: -64px -66px; width: 16px; height: 11px; } -.famfamfam-flag-tc { background-position: -80px 0px; width: 16px; height: 11px; } -.famfamfam-flag-sz { background-position: -80px -11px; width: 16px; height: 11px; } -.famfamfam-flag-sy { background-position: -80px -22px; width: 16px; height: 11px; } -.famfamfam-flag-sx { background-position: -80px -33px; width: 16px; height: 11px; } -.famfamfam-flag-sv { background-position: -80px -44px; width: 16px; height: 11px; } -.famfamfam-flag-st { background-position: -80px -55px; width: 16px; height: 11px; } -.famfamfam-flag-ss { background-position: -80px -66px; width: 16px; height: 11px; } -.famfamfam-flag-sr { background-position: 0px -77px; width: 16px; height: 11px; } -.famfamfam-flag-so { background-position: -16px -77px; width: 16px; height: 11px; } -.famfamfam-flag-sn { background-position: -32px -77px; width: 16px; height: 11px; } -.famfamfam-flag-sm { background-position: -48px -77px; width: 16px; height: 11px; } -.famfamfam-flag-sl { background-position: -64px -77px; width: 16px; height: 11px; } -.famfamfam-flag-sk { background-position: -80px -77px; width: 16px; height: 11px; } -.famfamfam-flag-si { background-position: -96px 0px; width: 16px; height: 11px; } -.famfamfam-flag-sh { background-position: -96px -11px; width: 16px; height: 11px; } -.famfamfam-flag-sg { background-position: -96px -22px; width: 16px; height: 11px; } -.famfamfam-flag-se { background-position: -96px -33px; width: 16px; height: 11px; } -.famfamfam-flag-sd { background-position: -96px -44px; width: 16px; height: 11px; } -.famfamfam-flag-scotland { background-position: -96px -55px; width: 16px; height: 11px; } -.famfamfam-flag-sc { background-position: -96px -66px; width: 16px; height: 11px; } -.famfamfam-flag-sb { background-position: -96px -77px; width: 16px; height: 11px; } -.famfamfam-flag-sa { background-position: 0px -88px; width: 16px; height: 11px; } -.famfamfam-flag-rw { background-position: -16px -88px; width: 16px; height: 11px; } -.famfamfam-flag-ru { background-position: -32px -88px; width: 16px; height: 11px; } -.famfamfam-flag-rs { background-position: -48px -88px; width: 16px; height: 11px; } -.famfamfam-flag-ro { background-position: -64px -88px; width: 16px; height: 11px; } -.famfamfam-flag-qa { background-position: -80px -88px; width: 16px; height: 11px; } -.famfamfam-flag-py { background-position: -96px -88px; width: 16px; height: 11px; } -.famfamfam-flag-pw { background-position: 0px -99px; width: 16px; height: 11px; } -.famfamfam-flag-pt { background-position: -16px -99px; width: 16px; height: 11px; } -.famfamfam-flag-ps { background-position: -32px -99px; width: 16px; height: 11px; } -.famfamfam-flag-pr { background-position: -48px -99px; width: 16px; height: 11px; } -.famfamfam-flag-pn { background-position: -64px -99px; width: 16px; height: 11px; } -.famfamfam-flag-pm { background-position: -80px -99px; width: 16px; height: 11px; } -.famfamfam-flag-pl { background-position: -96px -99px; width: 16px; height: 11px; } -.famfamfam-flag-pk { background-position: -112px 0px; width: 16px; height: 11px; } -.famfamfam-flag-ph { background-position: -112px -11px; width: 16px; height: 11px; } -.famfamfam-flag-pg { background-position: -112px -22px; width: 16px; height: 11px; } -.famfamfam-flag-pf { background-position: -112px -33px; width: 16px; height: 11px; } -.famfamfam-flag-pe { background-position: -112px -44px; width: 16px; height: 11px; } -.famfamfam-flag-pa { background-position: -112px -55px; width: 16px; height: 11px; } -.famfamfam-flag-om { background-position: -112px -66px; width: 16px; height: 11px; } -.famfamfam-flag-nz { background-position: -112px -77px; width: 16px; height: 11px; } -.famfamfam-flag-nu { background-position: -112px -88px; width: 16px; height: 11px; } -.famfamfam-flag-nr { background-position: -112px -99px; width: 16px; height: 11px; } -.famfamfam-flag-no { background-position: 0px -110px; width: 16px; height: 11px; } -.famfamfam-flag-bv { background-position: 0px -110px; width: 16px; height: 11px; } -.famfamfam-flag-sj { background-position: 0px -110px; width: 16px; height: 11px; } -.famfamfam-flag-nl { background-position: -16px -110px; width: 16px; height: 11px; } -.famfamfam-flag-ni { background-position: -32px -110px; width: 16px; height: 11px; } -.famfamfam-flag-ng { background-position: -48px -110px; width: 16px; height: 11px; } -.famfamfam-flag-nf { background-position: -64px -110px; width: 16px; height: 11px; } -.famfamfam-flag-ne { background-position: -80px -110px; width: 16px; height: 11px; } -.famfamfam-flag-nc { background-position: -96px -110px; width: 16px; height: 11px; } -.famfamfam-flag-na { background-position: -112px -110px; width: 16px; height: 11px; } -.famfamfam-flag-mz { background-position: -128px 0px; width: 16px; height: 11px; } -.famfamfam-flag-my { background-position: -128px -11px; width: 16px; height: 11px; } -.famfamfam-flag-mx { background-position: -128px -22px; width: 16px; height: 11px; } -.famfamfam-flag-mw { background-position: -128px -33px; width: 16px; height: 11px; } -.famfamfam-flag-mv { background-position: -128px -44px; width: 16px; height: 11px; } -.famfamfam-flag-mu { background-position: -128px -55px; width: 16px; height: 11px; } -.famfamfam-flag-mt { background-position: -128px -66px; width: 16px; height: 11px; } -.famfamfam-flag-ms { background-position: -128px -77px; width: 16px; height: 11px; } -.famfamfam-flag-mr { background-position: -128px -88px; width: 16px; height: 11px; } -.famfamfam-flag-mq { background-position: -128px -99px; width: 16px; height: 11px; } -.famfamfam-flag-mp { background-position: -128px -110px; width: 16px; height: 11px; } -.famfamfam-flag-mo { background-position: 0px -121px; width: 16px; height: 11px; } -.famfamfam-flag-mn { background-position: -16px -121px; width: 16px; height: 11px; } -.famfamfam-flag-mm { background-position: -32px -121px; width: 16px; height: 11px; } -.famfamfam-flag-ml { background-position: -48px -121px; width: 16px; height: 11px; } -.famfamfam-flag-mk { background-position: -64px -121px; width: 16px; height: 11px; } -.famfamfam-flag-mh { background-position: -80px -121px; width: 16px; height: 11px; } -.famfamfam-flag-mg { background-position: -96px -121px; width: 16px; height: 11px; } -.famfamfam-flag-me { background-position: 0px -132px; width: 16px; height: 12px; } -.famfamfam-flag-md { background-position: -112px -121px; width: 16px; height: 11px; } -.famfamfam-flag-mc { background-position: -128px -121px; width: 16px; height: 11px; } -.famfamfam-flag-ma { background-position: -16px -132px; width: 16px; height: 11px; } -.famfamfam-flag-ly { background-position: -32px -132px; width: 16px; height: 11px; } -.famfamfam-flag-lv { background-position: -48px -132px; width: 16px; height: 11px; } -.famfamfam-flag-lu { background-position: -64px -132px; width: 16px; height: 11px; } -.famfamfam-flag-lt { background-position: -80px -132px; width: 16px; height: 11px; } -.famfamfam-flag-ls { background-position: -96px -132px; width: 16px; height: 11px; } -.famfamfam-flag-lr { background-position: -112px -132px; width: 16px; height: 11px; } -.famfamfam-flag-lk { background-position: -128px -132px; width: 16px; height: 11px; } -.famfamfam-flag-li { background-position: -144px 0px; width: 16px; height: 11px; } -.famfamfam-flag-lc { background-position: -144px -11px; width: 16px; height: 11px; } -.famfamfam-flag-lb { background-position: -144px -22px; width: 16px; height: 11px; } -.famfamfam-flag-la { background-position: -144px -33px; width: 16px; height: 11px; } -.famfamfam-flag-kz { background-position: -144px -44px; width: 16px; height: 11px; } -.famfamfam-flag-ky { background-position: -144px -55px; width: 16px; height: 11px; } -.famfamfam-flag-kw { background-position: -144px -66px; width: 16px; height: 11px; } -.famfamfam-flag-kr { background-position: -144px -77px; width: 16px; height: 11px; } -.famfamfam-flag-kp { background-position: -144px -88px; width: 16px; height: 11px; } -.famfamfam-flag-kn { background-position: -144px -99px; width: 16px; height: 11px; } -.famfamfam-flag-km { background-position: -144px -110px; width: 16px; height: 11px; } -.famfamfam-flag-ki { background-position: -144px -121px; width: 16px; height: 11px; } -.famfamfam-flag-kh { background-position: -144px -132px; width: 16px; height: 11px; } -.famfamfam-flag-kg { background-position: 0px -144px; width: 16px; height: 11px; } -.famfamfam-flag-ke { background-position: -16px -144px; width: 16px; height: 11px; } -.famfamfam-flag-jp { background-position: -32px -144px; width: 16px; height: 11px; } -.famfamfam-flag-jo { background-position: -48px -144px; width: 16px; height: 11px; } -.famfamfam-flag-jm { background-position: -64px -144px; width: 16px; height: 11px; } -.famfamfam-flag-je { background-position: -80px -144px; width: 16px; height: 11px; } -.famfamfam-flag-it { background-position: -96px -144px; width: 16px; height: 11px; } -.famfamfam-flag-is { background-position: -112px -144px; width: 16px; height: 11px; } -.famfamfam-flag-ir { background-position: -128px -144px; width: 16px; height: 11px; } -.famfamfam-flag-iq { background-position: -144px -144px; width: 16px; height: 11px; } -.famfamfam-flag-io { background-position: -160px 0px; width: 16px; height: 11px; } -.famfamfam-flag-in { background-position: -160px -11px; width: 16px; height: 11px; } -.famfamfam-flag-im { background-position: -160px -22px; width: 16px; height: 9px; } -.famfamfam-flag-il { background-position: -160px -31px; width: 16px; height: 11px; } -.famfamfam-flag-ie { background-position: -160px -42px; width: 16px; height: 11px; } -.famfamfam-flag-id { background-position: -160px -53px; width: 16px; height: 11px; } -.famfamfam-flag-hu { background-position: -160px -64px; width: 16px; height: 11px; } -.famfamfam-flag-ht { background-position: -160px -75px; width: 16px; height: 11px; } -.famfamfam-flag-hr { background-position: -160px -86px; width: 16px; height: 11px; } -.famfamfam-flag-hn { background-position: -160px -97px; width: 16px; height: 11px; } -.famfamfam-flag-hk { background-position: -160px -108px; width: 16px; height: 11px; } -.famfamfam-flag-gy { background-position: -160px -119px; width: 16px; height: 11px; } -.famfamfam-flag-gw { background-position: -160px -130px; width: 16px; height: 11px; } -.famfamfam-flag-gu { background-position: -160px -141px; width: 16px; height: 11px; } -.famfamfam-flag-gt { background-position: 0px -155px; width: 16px; height: 11px; } -.famfamfam-flag-gs { background-position: -16px -155px; width: 16px; height: 11px; } -.famfamfam-flag-gr { background-position: -32px -155px; width: 16px; height: 11px; } -.famfamfam-flag-gq { background-position: -48px -155px; width: 16px; height: 11px; } -.famfamfam-flag-gp { background-position: -64px -155px; width: 16px; height: 11px; } -.famfamfam-flag-gn { background-position: -80px -155px; width: 16px; height: 11px; } -.famfamfam-flag-gm { background-position: -96px -155px; width: 16px; height: 11px; } -.famfamfam-flag-gl { background-position: -112px -155px; width: 16px; height: 11px; } -.famfamfam-flag-gi { background-position: -128px -155px; width: 16px; height: 11px; } -.famfamfam-flag-gh { background-position: -144px -155px; width: 16px; height: 11px; } -.famfamfam-flag-gg { background-position: -160px -155px; width: 16px; height: 11px; } -.famfamfam-flag-ge { background-position: -176px 0px; width: 16px; height: 11px; } -.famfamfam-flag-gd { background-position: -176px -11px; width: 16px; height: 11px; } -.famfamfam-flag-gb { background-position: -176px -22px; width: 16px; height: 11px; } -.famfamfam-flag-ga { background-position: -176px -33px; width: 16px; height: 11px; } -.famfamfam-flag-fr { background-position: -176px -44px; width: 16px; height: 11px; } -.famfamfam-flag-gf { background-position: -176px -44px; width: 16px; height: 11px; } -.famfamfam-flag-re { background-position: -176px -44px; width: 16px; height: 11px; } -.famfamfam-flag-mf { background-position: -176px -44px; width: 16px; height: 11px; } -.famfamfam-flag-bl { background-position: -176px -44px; width: 16px; height: 11px; } -.famfamfam-flag-fo { background-position: -176px -55px; width: 16px; height: 11px; } -.famfamfam-flag-fm { background-position: -176px -66px; width: 16px; height: 11px; } -.famfamfam-flag-fk { background-position: -176px -77px; width: 16px; height: 11px; } -.famfamfam-flag-fj { background-position: -176px -88px; width: 16px; height: 11px; } -.famfamfam-flag-fi { background-position: -176px -99px; width: 16px; height: 11px; } -.famfamfam-flag-fam { background-position: -176px -110px; width: 16px; height: 11px; } -.famfamfam-flag-eu { background-position: -176px -121px; width: 16px; height: 11px; } -.famfamfam-flag-et { background-position: -176px -132px; width: 16px; height: 11px; } -.famfamfam-flag-es { background-position: -176px -143px; width: 16px; height: 11px; } -.famfamfam-flag-er { background-position: -176px -154px; width: 16px; height: 11px; } -.famfamfam-flag-england { background-position: 0px -166px; width: 16px; height: 11px; } -.famfamfam-flag-eh { background-position: -16px -166px; width: 16px; height: 11px; } -.famfamfam-flag-eg { background-position: -32px -166px; width: 16px; height: 11px; } -.famfamfam-flag-ee { background-position: -48px -166px; width: 16px; height: 11px; } -.famfamfam-flag-ec { background-position: -64px -166px; width: 16px; height: 11px; } -.famfamfam-flag-dz { background-position: -80px -166px; width: 16px; height: 11px; } -.famfamfam-flag-do { background-position: -96px -166px; width: 16px; height: 11px; } -.famfamfam-flag-dm { background-position: -112px -166px; width: 16px; height: 11px; } -.famfamfam-flag-dk { background-position: -128px -166px; width: 16px; height: 11px; } -.famfamfam-flag-dj { background-position: -144px -166px; width: 16px; height: 11px; } -.famfamfam-flag-de { background-position: -160px -166px; width: 16px; height: 11px; } -.famfamfam-flag-cz { background-position: -176px -166px; width: 16px; height: 11px; } -.famfamfam-flag-cy { background-position: 0px -177px; width: 16px; height: 11px; } -.famfamfam-flag-cx { background-position: -16px -177px; width: 16px; height: 11px; } -.famfamfam-flag-cw { background-position: -32px -177px; width: 16px; height: 11px; } -.famfamfam-flag-cv { background-position: -48px -177px; width: 16px; height: 11px; } -.famfamfam-flag-cu { background-position: -64px -177px; width: 16px; height: 11px; } -.famfamfam-flag-cs { background-position: -80px -177px; width: 16px; height: 11px; } -.famfamfam-flag-cr { background-position: -96px -177px; width: 16px; height: 11px; } -.famfamfam-flag-co { background-position: -112px -177px; width: 16px; height: 11px; } -.famfamfam-flag-cn { background-position: -128px -177px; width: 16px; height: 11px; } -.famfamfam-flag-cm { background-position: -144px -177px; width: 16px; height: 11px; } -.famfamfam-flag-cl { background-position: -160px -177px; width: 16px; height: 11px; } -.famfamfam-flag-ck { background-position: -176px -177px; width: 16px; height: 11px; } -.famfamfam-flag-ci { background-position: -192px 0px; width: 16px; height: 11px; } -.famfamfam-flag-cg { background-position: -192px -11px; width: 16px; height: 11px; } -.famfamfam-flag-cf { background-position: -192px -22px; width: 16px; height: 11px; } -.famfamfam-flag-cd { background-position: -192px -33px; width: 16px; height: 11px; } -.famfamfam-flag-cc { background-position: -192px -44px; width: 16px; height: 11px; } -.famfamfam-flag-catalonia { background-position: -192px -55px; width: 16px; height: 11px; } -.famfamfam-flag-ca { background-position: -192px -66px; width: 16px; height: 11px; } -.famfamfam-flag-bz { background-position: -192px -77px; width: 16px; height: 11px; } -.famfamfam-flag-by { background-position: -192px -88px; width: 16px; height: 11px; } -.famfamfam-flag-bw { background-position: -192px -99px; width: 16px; height: 11px; } -.famfamfam-flag-bt { background-position: -192px -110px; width: 16px; height: 11px; } -.famfamfam-flag-bs { background-position: -192px -121px; width: 16px; height: 11px; } -.famfamfam-flag-br { background-position: -192px -132px; width: 16px; height: 11px; } -.famfamfam-flag-bq { background-position: -192px -143px; width: 16px; height: 11px; } -.famfamfam-flag-bo { background-position: -192px -154px; width: 16px; height: 11px; } -.famfamfam-flag-bn { background-position: -192px -165px; width: 16px; height: 11px; } -.famfamfam-flag-bm { background-position: -192px -176px; width: 16px; height: 11px; } -.famfamfam-flag-bj { background-position: 0px -188px; width: 16px; height: 11px; } -.famfamfam-flag-bi { background-position: -16px -188px; width: 16px; height: 11px; } -.famfamfam-flag-bh { background-position: -32px -188px; width: 16px; height: 11px; } -.famfamfam-flag-bg { background-position: -48px -188px; width: 16px; height: 11px; } -.famfamfam-flag-bf { background-position: -64px -188px; width: 16px; height: 11px; } -.famfamfam-flag-be { background-position: -80px -188px; width: 16px; height: 11px; } -.famfamfam-flag-bd { background-position: -96px -188px; width: 16px; height: 11px; } -.famfamfam-flag-bb { background-position: -112px -188px; width: 16px; height: 11px; } -.famfamfam-flag-ba { background-position: -128px -188px; width: 16px; height: 11px; } -.famfamfam-flag-az { background-position: -144px -188px; width: 16px; height: 11px; } -.famfamfam-flag-ax { background-position: -160px -188px; width: 16px; height: 11px; } -.famfamfam-flag-aw { background-position: -176px -188px; width: 16px; height: 11px; } -.famfamfam-flag-au { background-position: -192px -188px; width: 16px; height: 11px; } -.famfamfam-flag-hm { background-position: -192px -188px; width: 16px; height: 11px; } -.famfamfam-flag-at { background-position: -208px 0px; width: 16px; height: 11px; } -.famfamfam-flag-as { background-position: -208px -11px; width: 16px; height: 11px; } -.famfamfam-flag-ar { background-position: -208px -22px; width: 16px; height: 11px; } -.famfamfam-flag-ao { background-position: -208px -33px; width: 16px; height: 11px; } -.famfamfam-flag-an { background-position: -208px -44px; width: 16px; height: 11px; } -.famfamfam-flag-am { background-position: -208px -55px; width: 16px; height: 11px; } -.famfamfam-flag-al { background-position: -208px -66px; width: 16px; height: 11px; } -.famfamfam-flag-ai { background-position: -208px -77px; width: 16px; height: 11px; } -.famfamfam-flag-ag { background-position: -208px -88px; width: 16px; height: 11px; } -.famfamfam-flag-af { background-position: -208px -99px; width: 16px; height: 11px; } -.famfamfam-flag-ae { background-position: -208px -110px; width: 16px; height: 11px; } -.famfamfam-flag-ad { background-position: -208px -121px; width: 16px; height: 11px; } -.famfamfam-flag-np { background-position: -208px -132px; width: 9px; height: 11px; } -.famfamfam-flag-ch { background-position: -208px -143px; width: 11px; height: 11px; } +.fam-flag-zw { background-position: 0px 0px; } +.fam-flag-zm { background-position: -16px 0px; } +.fam-flag-za { background-position: 0px -11px; } +.fam-flag-yt { background-position: -16px -11px; } +.fam-flag-ye { background-position: -32px 0px; } +.fam-flag-ws { background-position: -32px -11px; } +.fam-flag-wf { background-position: 0px -22px; } +.fam-flag-wales { background-position: -16px -22px; } +.fam-flag-vu { background-position: -32px -22px; } +.fam-flag-vn { background-position: 0px -33px; } +.fam-flag-vi { background-position: -16px -33px; } +.fam-flag-vg { background-position: -32px -33px; } +.fam-flag-ve { background-position: -48px 0px; } +.fam-flag-vc { background-position: -48px -11px; } +.fam-flag-va { background-position: -48px -22px; } +.fam-flag-uz { background-position: -48px -33px; } +.fam-flag-uy { background-position: 0px -44px; } +.fam-flag-us { background-position: -16px -44px; } +.fam-flag-um { background-position: -16px -44px; } +.fam-flag-ug { background-position: -32px -44px; } +.fam-flag-ua { background-position: -48px -44px; } +.fam-flag-tz { background-position: -64px 0px; } +.fam-flag-tw { background-position: -64px -11px; } +.fam-flag-tv { background-position: -64px -22px; } +.fam-flag-tt { background-position: -64px -33px; } +.fam-flag-tr { background-position: -64px -44px; } +.fam-flag-to { background-position: 0px -55px; } +.fam-flag-tn { background-position: -16px -55px; } +.fam-flag-tm { background-position: -32px -55px; } +.fam-flag-tl { background-position: -48px -55px; } +.fam-flag-tk { background-position: -64px -55px; } +.fam-flag-tj { background-position: 0px -66px; } +.fam-flag-th { background-position: -16px -66px; } +.fam-flag-tg { background-position: -32px -66px; } +.fam-flag-tf { background-position: -48px -66px; } +.fam-flag-td { background-position: -64px -66px; } +.fam-flag-tc { background-position: -80px 0px; } +.fam-flag-sz { background-position: -80px -11px; } +.fam-flag-sy { background-position: -80px -22px; } +.fam-flag-sx { background-position: -80px -33px; } +.fam-flag-sv { background-position: -80px -44px; } +.fam-flag-st { background-position: -80px -55px; } +.fam-flag-ss { background-position: -80px -66px; } +.fam-flag-sr { background-position: 0px -77px; } +.fam-flag-so { background-position: -16px -77px; } +.fam-flag-sn { background-position: -32px -77px; } +.fam-flag-sm { background-position: -48px -77px; } +.fam-flag-sl { background-position: -64px -77px; } +.fam-flag-sk { background-position: -80px -77px; } +.fam-flag-si { background-position: -96px 0px; } +.fam-flag-sh { background-position: -96px -11px; } +.fam-flag-sg { background-position: -96px -22px; } +.fam-flag-se { background-position: -96px -33px; } +.fam-flag-sd { background-position: -96px -44px; } +.fam-flag-scotland { background-position: -96px -55px; } +.fam-flag-sc { background-position: -96px -66px; } +.fam-flag-sb { background-position: -96px -77px; } +.fam-flag-sa { background-position: 0px -88px; } +.fam-flag-rw { background-position: -16px -88px; } +.fam-flag-ru { background-position: -32px -88px; } +.fam-flag-rs { background-position: -48px -88px; } +.fam-flag-ro { background-position: -64px -88px; } +.fam-flag-qa { background-position: -80px -88px; } +.fam-flag-py { background-position: -96px -88px; } +.fam-flag-pw { background-position: 0px -99px; } +.fam-flag-pt { background-position: -16px -99px; } +.fam-flag-ps { background-position: -32px -99px; } +.fam-flag-pr { background-position: -48px -99px; } +.fam-flag-pn { background-position: -64px -99px; } +.fam-flag-pm { background-position: -80px -99px; } +.fam-flag-pl { background-position: -96px -99px; } +.fam-flag-pk { background-position: -112px 0px; } +.fam-flag-ph { background-position: -112px -11px; } +.fam-flag-pg { background-position: -112px -22px; } +.fam-flag-pf { background-position: -112px -33px; } +.fam-flag-pe { background-position: -112px -44px; } +.fam-flag-pa { background-position: -112px -55px; } +.fam-flag-om { background-position: -112px -66px; } +.fam-flag-nz { background-position: -112px -77px; } +.fam-flag-nu { background-position: -112px -88px; } +.fam-flag-nr { background-position: -112px -99px; } +.fam-flag-no { background-position: 0px -110px; } +.fam-flag-bv { background-position: 0px -110px; } +.fam-flag-sj { background-position: 0px -110px; } +.fam-flag-nl { background-position: -16px -110px; } +.fam-flag-ni { background-position: -32px -110px; } +.fam-flag-ng { background-position: -48px -110px; } +.fam-flag-nf { background-position: -64px -110px; } +.fam-flag-ne { background-position: -80px -110px; } +.fam-flag-nc { background-position: -96px -110px; } +.fam-flag-na { background-position: -112px -110px; } +.fam-flag-mz { background-position: -128px 0px; } +.fam-flag-my { background-position: -128px -11px; } +.fam-flag-mx { background-position: -128px -22px; } +.fam-flag-mw { background-position: -128px -33px; } +.fam-flag-mv { background-position: -128px -44px; } +.fam-flag-mu { background-position: -128px -55px; } +.fam-flag-mt { background-position: -128px -66px; } +.fam-flag-ms { background-position: -128px -77px; } +.fam-flag-mr { background-position: -128px -88px; } +.fam-flag-mq { background-position: -128px -99px; } +.fam-flag-mp { background-position: -128px -110px; } +.fam-flag-mo { background-position: 0px -121px; } +.fam-flag-mn { background-position: -16px -121px; } +.fam-flag-mm { background-position: -32px -121px; } +.fam-flag-ml { background-position: -48px -121px; } +.fam-flag-mk { background-position: -64px -121px; } +.fam-flag-mh { background-position: -80px -121px; } +.fam-flag-mg { background-position: -96px -121px; } +.fam-flag-me { background-position: 0px -132px; width: 16px; height: 12px; } +.fam-flag-md { background-position: -112px -121px; } +.fam-flag-mc { background-position: -128px -121px; } +.fam-flag-ma { background-position: -16px -132px; } +.fam-flag-ly { background-position: -32px -132px; } +.fam-flag-lv { background-position: -48px -132px; } +.fam-flag-lu { background-position: -64px -132px; } +.fam-flag-lt { background-position: -80px -132px; } +.fam-flag-ls { background-position: -96px -132px; } +.fam-flag-lr { background-position: -112px -132px; } +.fam-flag-lk { background-position: -128px -132px; } +.fam-flag-li { background-position: -144px 0px; } +.fam-flag-lc { background-position: -144px -11px; } +.fam-flag-lb { background-position: -144px -22px; } +.fam-flag-la { background-position: -144px -33px; } +.fam-flag-kz { background-position: -144px -44px; } +.fam-flag-ky { background-position: -144px -55px; } +.fam-flag-kw { background-position: -144px -66px; } +.fam-flag-kr { background-position: -144px -77px; } +.fam-flag-kp { background-position: -144px -88px; } +.fam-flag-kn { background-position: -144px -99px; } +.fam-flag-km { background-position: -144px -110px; } +.fam-flag-ki { background-position: -144px -121px; } +.fam-flag-kh { background-position: -144px -132px; } +.fam-flag-kg { background-position: 0px -144px; } +.fam-flag-ke { background-position: -16px -144px; } +.fam-flag-jp { background-position: -32px -144px; } +.fam-flag-jo { background-position: -48px -144px; } +.fam-flag-jm { background-position: -64px -144px; } +.fam-flag-je { background-position: -80px -144px; } +.fam-flag-it { background-position: -96px -144px; } +.fam-flag-is { background-position: -112px -144px; } +.fam-flag-ir { background-position: -128px -144px; } +.fam-flag-iq { background-position: -144px -144px; } +.fam-flag-io { background-position: -160px 0px; } +.fam-flag-in { background-position: -160px -11px; } +.fam-flag-im { background-position: -160px -22px; width: 16px; height: 9px; } +.fam-flag-il { background-position: -160px -31px; } +.fam-flag-ie { background-position: -160px -42px; } +.fam-flag-id { background-position: -160px -53px; } +.fam-flag-hu { background-position: -160px -64px; } +.fam-flag-ht { background-position: -160px -75px; } +.fam-flag-hr { background-position: -160px -86px; } +.fam-flag-hn { background-position: -160px -97px; } +.fam-flag-hk { background-position: -160px -108px; } +.fam-flag-gy { background-position: -160px -119px; } +.fam-flag-gw { background-position: -160px -130px; } +.fam-flag-gu { background-position: -160px -141px; } +.fam-flag-gt { background-position: 0px -155px; } +.fam-flag-gs { background-position: -16px -155px; } +.fam-flag-gr { background-position: -32px -155px; } +.fam-flag-gq { background-position: -48px -155px; } +.fam-flag-gp { background-position: -64px -155px; } +.fam-flag-gn { background-position: -80px -155px; } +.fam-flag-gm { background-position: -96px -155px; } +.fam-flag-gl { background-position: -112px -155px; } +.fam-flag-gi { background-position: -128px -155px; } +.fam-flag-gh { background-position: -144px -155px; } +.fam-flag-gg { background-position: -160px -155px; } +.fam-flag-ge { background-position: -176px 0px; } +.fam-flag-gd { background-position: -176px -11px; } +.fam-flag-gb { background-position: -176px -22px; } +.fam-flag-ga { background-position: -176px -33px; } +.fam-flag-fr { background-position: -176px -44px; } +.fam-flag-gf { background-position: -176px -44px; } +.fam-flag-re { background-position: -176px -44px; } +.fam-flag-mf { background-position: -176px -44px; } +.fam-flag-bl { background-position: -176px -44px; } +.fam-flag-fo { background-position: -176px -55px; } +.fam-flag-fm { background-position: -176px -66px; } +.fam-flag-fk { background-position: -176px -77px; } +.fam-flag-fj { background-position: -176px -88px; } +.fam-flag-fi { background-position: -176px -99px; } +.fam-flag-fam { background-position: -176px -110px; } +.fam-flag-eu { background-position: -176px -121px; } +.fam-flag-et { background-position: -176px -132px; } +.fam-flag-es { background-position: -176px -143px; } +.fam-flag-er { background-position: -176px -154px; } +.fam-flag-england { background-position: 0px -166px; } +.fam-flag-eh { background-position: -16px -166px; } +.fam-flag-eg { background-position: -32px -166px; } +.fam-flag-ee { background-position: -48px -166px; } +.fam-flag-ec { background-position: -64px -166px; } +.fam-flag-dz { background-position: -80px -166px; } +.fam-flag-do { background-position: -96px -166px; } +.fam-flag-dm { background-position: -112px -166px; } +.fam-flag-dk { background-position: -128px -166px; } +.fam-flag-dj { background-position: -144px -166px; } +.fam-flag-de { background-position: -160px -166px; } +.fam-flag-cz { background-position: -176px -166px; } +.fam-flag-cy { background-position: 0px -177px; } +.fam-flag-cx { background-position: -16px -177px; } +.fam-flag-cw { background-position: -32px -177px; } +.fam-flag-cv { background-position: -48px -177px; } +.fam-flag-cu { background-position: -64px -177px; } +.fam-flag-cs { background-position: -80px -177px; } +.fam-flag-cr { background-position: -96px -177px; } +.fam-flag-co { background-position: -112px -177px; } +.fam-flag-cn { background-position: -128px -177px; } +.fam-flag-cm { background-position: -144px -177px; } +.fam-flag-cl { background-position: -160px -177px; } +.fam-flag-ck { background-position: -176px -177px; } +.fam-flag-ci { background-position: -192px 0px; } +.fam-flag-cg { background-position: -192px -11px; } +.fam-flag-cf { background-position: -192px -22px; } +.fam-flag-cd { background-position: -192px -33px; } +.fam-flag-cc { background-position: -192px -44px; } +.fam-flag-catalonia { background-position: -192px -55px; } +.fam-flag-ca { background-position: -192px -66px; } +.fam-flag-bz { background-position: -192px -77px; } +.fam-flag-by { background-position: -192px -88px; } +.fam-flag-bw { background-position: -192px -99px; } +.fam-flag-bt { background-position: -192px -110px; } +.fam-flag-bs { background-position: -192px -121px; } +.fam-flag-br { background-position: -192px -132px; } +.fam-flag-bq { background-position: -192px -143px; } +.fam-flag-bo { background-position: -192px -154px; } +.fam-flag-bn { background-position: -192px -165px; } +.fam-flag-bm { background-position: -192px -176px; } +.fam-flag-bj { background-position: 0px -188px; } +.fam-flag-bi { background-position: -16px -188px; } +.fam-flag-bh { background-position: -32px -188px; } +.fam-flag-bg { background-position: -48px -188px; } +.fam-flag-bf { background-position: -64px -188px; } +.fam-flag-be { background-position: -80px -188px; } +.fam-flag-bd { background-position: -96px -188px; } +.fam-flag-bb { background-position: -112px -188px; } +.fam-flag-ba { background-position: -128px -188px; } +.fam-flag-az { background-position: -144px -188px; } +.fam-flag-ax { background-position: -160px -188px; } +.fam-flag-aw { background-position: -176px -188px; } +.fam-flag-au { background-position: -192px -188px; } +.fam-flag-hm { background-position: -192px -188px; } +.fam-flag-at { background-position: -208px 0px; } +.fam-flag-as { background-position: -208px -11px; } +.fam-flag-ar { background-position: -208px -22px; } +.fam-flag-ao { background-position: -208px -33px; } +.fam-flag-an { background-position: -208px -44px; } +.fam-flag-am { background-position: -208px -55px; } +.fam-flag-al { background-position: -208px -66px; } +.fam-flag-ai { background-position: -208px -77px; } +.fam-flag-ag { background-position: -208px -88px; } +.fam-flag-af { background-position: -208px -99px; } +.fam-flag-ae { background-position: -208px -110px; } +.fam-flag-ad { background-position: -208px -121px; } +.fam-flag-np { background-position: -208px -132px; width: 9px; } +.fam-flag-ch { background-position: -208px -143px; width: 11px; } -- cgit v1.2.3-54-g00ecf From d5816a597d2db1742919d9492ffca758019dcc74 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 16 Jan 2013 00:05:07 -0600 Subject: More famfamfam flags style tweaking * Point default image at a blank spot in the PNG. * Remove dead style. * Don't use any crazy prefix matching stuff; just have separate class for default view. Signed-off-by: Dan McGee --- sitestatic/famfamfam-flags.css | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sitestatic/famfamfam-flags.css b/sitestatic/famfamfam-flags.css index b8e6ab5a..ea03eb5c 100644 --- a/sitestatic/famfamfam-flags.css +++ b/sitestatic/famfamfam-flags.css @@ -2,14 +2,13 @@ * fam flag icons CSS. */ -[class^="fam-flag"] { +.fam-flag { display: inline-block; width: 16px; height: 11px; line-height: 11px; - /* vertical-align: text-top; */ background-image: url("famfamfam-flags.png"); - background-position: 0 0; + background-position: -208px -188px; background-repeat: no-repeat; } -- cgit v1.2.3-54-g00ecf From 06129da6bfcfe7c0904559a4326e323286bdcc30 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 16 Jan 2013 00:07:07 -0600 Subject: Rename famfamfam flags sprite PNG and CSS Signed-off-by: Dan McGee --- sitestatic/famfamfam-flags.css | 270 ----------------------------------------- sitestatic/famfamfam-flags.png | Bin 76543 -> 0 bytes sitestatic/flags/fam.css | 270 +++++++++++++++++++++++++++++++++++++++++ sitestatic/flags/fam.png | Bin 0 -> 76543 bytes 4 files changed, 270 insertions(+), 270 deletions(-) delete mode 100644 sitestatic/famfamfam-flags.css delete mode 100644 sitestatic/famfamfam-flags.png create mode 100644 sitestatic/flags/fam.css create mode 100644 sitestatic/flags/fam.png diff --git a/sitestatic/famfamfam-flags.css b/sitestatic/famfamfam-flags.css deleted file mode 100644 index ea03eb5c..00000000 --- a/sitestatic/famfamfam-flags.css +++ /dev/null @@ -1,270 +0,0 @@ -/** - * fam flag icons CSS. - */ - -.fam-flag { - display: inline-block; - width: 16px; - height: 11px; - line-height: 11px; - background-image: url("famfamfam-flags.png"); - background-position: -208px -188px; - background-repeat: no-repeat; -} - -.fam-flag-zw { background-position: 0px 0px; } -.fam-flag-zm { background-position: -16px 0px; } -.fam-flag-za { background-position: 0px -11px; } -.fam-flag-yt { background-position: -16px -11px; } -.fam-flag-ye { background-position: -32px 0px; } -.fam-flag-ws { background-position: -32px -11px; } -.fam-flag-wf { background-position: 0px -22px; } -.fam-flag-wales { background-position: -16px -22px; } -.fam-flag-vu { background-position: -32px -22px; } -.fam-flag-vn { background-position: 0px -33px; } -.fam-flag-vi { background-position: -16px -33px; } -.fam-flag-vg { background-position: -32px -33px; } -.fam-flag-ve { background-position: -48px 0px; } -.fam-flag-vc { background-position: -48px -11px; } -.fam-flag-va { background-position: -48px -22px; } -.fam-flag-uz { background-position: -48px -33px; } -.fam-flag-uy { background-position: 0px -44px; } -.fam-flag-us { background-position: -16px -44px; } -.fam-flag-um { background-position: -16px -44px; } -.fam-flag-ug { background-position: -32px -44px; } -.fam-flag-ua { background-position: -48px -44px; } -.fam-flag-tz { background-position: -64px 0px; } -.fam-flag-tw { background-position: -64px -11px; } -.fam-flag-tv { background-position: -64px -22px; } -.fam-flag-tt { background-position: -64px -33px; } -.fam-flag-tr { background-position: -64px -44px; } -.fam-flag-to { background-position: 0px -55px; } -.fam-flag-tn { background-position: -16px -55px; } -.fam-flag-tm { background-position: -32px -55px; } -.fam-flag-tl { background-position: -48px -55px; } -.fam-flag-tk { background-position: -64px -55px; } -.fam-flag-tj { background-position: 0px -66px; } -.fam-flag-th { background-position: -16px -66px; } -.fam-flag-tg { background-position: -32px -66px; } -.fam-flag-tf { background-position: -48px -66px; } -.fam-flag-td { background-position: -64px -66px; } -.fam-flag-tc { background-position: -80px 0px; } -.fam-flag-sz { background-position: -80px -11px; } -.fam-flag-sy { background-position: -80px -22px; } -.fam-flag-sx { background-position: -80px -33px; } -.fam-flag-sv { background-position: -80px -44px; } -.fam-flag-st { background-position: -80px -55px; } -.fam-flag-ss { background-position: -80px -66px; } -.fam-flag-sr { background-position: 0px -77px; } -.fam-flag-so { background-position: -16px -77px; } -.fam-flag-sn { background-position: -32px -77px; } -.fam-flag-sm { background-position: -48px -77px; } -.fam-flag-sl { background-position: -64px -77px; } -.fam-flag-sk { background-position: -80px -77px; } -.fam-flag-si { background-position: -96px 0px; } -.fam-flag-sh { background-position: -96px -11px; } -.fam-flag-sg { background-position: -96px -22px; } -.fam-flag-se { background-position: -96px -33px; } -.fam-flag-sd { background-position: -96px -44px; } -.fam-flag-scotland { background-position: -96px -55px; } -.fam-flag-sc { background-position: -96px -66px; } -.fam-flag-sb { background-position: -96px -77px; } -.fam-flag-sa { background-position: 0px -88px; } -.fam-flag-rw { background-position: -16px -88px; } -.fam-flag-ru { background-position: -32px -88px; } -.fam-flag-rs { background-position: -48px -88px; } -.fam-flag-ro { background-position: -64px -88px; } -.fam-flag-qa { background-position: -80px -88px; } -.fam-flag-py { background-position: -96px -88px; } -.fam-flag-pw { background-position: 0px -99px; } -.fam-flag-pt { background-position: -16px -99px; } -.fam-flag-ps { background-position: -32px -99px; } -.fam-flag-pr { background-position: -48px -99px; } -.fam-flag-pn { background-position: -64px -99px; } -.fam-flag-pm { background-position: -80px -99px; } -.fam-flag-pl { background-position: -96px -99px; } -.fam-flag-pk { background-position: -112px 0px; } -.fam-flag-ph { background-position: -112px -11px; } -.fam-flag-pg { background-position: -112px -22px; } -.fam-flag-pf { background-position: -112px -33px; } -.fam-flag-pe { background-position: -112px -44px; } -.fam-flag-pa { background-position: -112px -55px; } -.fam-flag-om { background-position: -112px -66px; } -.fam-flag-nz { background-position: -112px -77px; } -.fam-flag-nu { background-position: -112px -88px; } -.fam-flag-nr { background-position: -112px -99px; } -.fam-flag-no { background-position: 0px -110px; } -.fam-flag-bv { background-position: 0px -110px; } -.fam-flag-sj { background-position: 0px -110px; } -.fam-flag-nl { background-position: -16px -110px; } -.fam-flag-ni { background-position: -32px -110px; } -.fam-flag-ng { background-position: -48px -110px; } -.fam-flag-nf { background-position: -64px -110px; } -.fam-flag-ne { background-position: -80px -110px; } -.fam-flag-nc { background-position: -96px -110px; } -.fam-flag-na { background-position: -112px -110px; } -.fam-flag-mz { background-position: -128px 0px; } -.fam-flag-my { background-position: -128px -11px; } -.fam-flag-mx { background-position: -128px -22px; } -.fam-flag-mw { background-position: -128px -33px; } -.fam-flag-mv { background-position: -128px -44px; } -.fam-flag-mu { background-position: -128px -55px; } -.fam-flag-mt { background-position: -128px -66px; } -.fam-flag-ms { background-position: -128px -77px; } -.fam-flag-mr { background-position: -128px -88px; } -.fam-flag-mq { background-position: -128px -99px; } -.fam-flag-mp { background-position: -128px -110px; } -.fam-flag-mo { background-position: 0px -121px; } -.fam-flag-mn { background-position: -16px -121px; } -.fam-flag-mm { background-position: -32px -121px; } -.fam-flag-ml { background-position: -48px -121px; } -.fam-flag-mk { background-position: -64px -121px; } -.fam-flag-mh { background-position: -80px -121px; } -.fam-flag-mg { background-position: -96px -121px; } -.fam-flag-me { background-position: 0px -132px; width: 16px; height: 12px; } -.fam-flag-md { background-position: -112px -121px; } -.fam-flag-mc { background-position: -128px -121px; } -.fam-flag-ma { background-position: -16px -132px; } -.fam-flag-ly { background-position: -32px -132px; } -.fam-flag-lv { background-position: -48px -132px; } -.fam-flag-lu { background-position: -64px -132px; } -.fam-flag-lt { background-position: -80px -132px; } -.fam-flag-ls { background-position: -96px -132px; } -.fam-flag-lr { background-position: -112px -132px; } -.fam-flag-lk { background-position: -128px -132px; } -.fam-flag-li { background-position: -144px 0px; } -.fam-flag-lc { background-position: -144px -11px; } -.fam-flag-lb { background-position: -144px -22px; } -.fam-flag-la { background-position: -144px -33px; } -.fam-flag-kz { background-position: -144px -44px; } -.fam-flag-ky { background-position: -144px -55px; } -.fam-flag-kw { background-position: -144px -66px; } -.fam-flag-kr { background-position: -144px -77px; } -.fam-flag-kp { background-position: -144px -88px; } -.fam-flag-kn { background-position: -144px -99px; } -.fam-flag-km { background-position: -144px -110px; } -.fam-flag-ki { background-position: -144px -121px; } -.fam-flag-kh { background-position: -144px -132px; } -.fam-flag-kg { background-position: 0px -144px; } -.fam-flag-ke { background-position: -16px -144px; } -.fam-flag-jp { background-position: -32px -144px; } -.fam-flag-jo { background-position: -48px -144px; } -.fam-flag-jm { background-position: -64px -144px; } -.fam-flag-je { background-position: -80px -144px; } -.fam-flag-it { background-position: -96px -144px; } -.fam-flag-is { background-position: -112px -144px; } -.fam-flag-ir { background-position: -128px -144px; } -.fam-flag-iq { background-position: -144px -144px; } -.fam-flag-io { background-position: -160px 0px; } -.fam-flag-in { background-position: -160px -11px; } -.fam-flag-im { background-position: -160px -22px; width: 16px; height: 9px; } -.fam-flag-il { background-position: -160px -31px; } -.fam-flag-ie { background-position: -160px -42px; } -.fam-flag-id { background-position: -160px -53px; } -.fam-flag-hu { background-position: -160px -64px; } -.fam-flag-ht { background-position: -160px -75px; } -.fam-flag-hr { background-position: -160px -86px; } -.fam-flag-hn { background-position: -160px -97px; } -.fam-flag-hk { background-position: -160px -108px; } -.fam-flag-gy { background-position: -160px -119px; } -.fam-flag-gw { background-position: -160px -130px; } -.fam-flag-gu { background-position: -160px -141px; } -.fam-flag-gt { background-position: 0px -155px; } -.fam-flag-gs { background-position: -16px -155px; } -.fam-flag-gr { background-position: -32px -155px; } -.fam-flag-gq { background-position: -48px -155px; } -.fam-flag-gp { background-position: -64px -155px; } -.fam-flag-gn { background-position: -80px -155px; } -.fam-flag-gm { background-position: -96px -155px; } -.fam-flag-gl { background-position: -112px -155px; } -.fam-flag-gi { background-position: -128px -155px; } -.fam-flag-gh { background-position: -144px -155px; } -.fam-flag-gg { background-position: -160px -155px; } -.fam-flag-ge { background-position: -176px 0px; } -.fam-flag-gd { background-position: -176px -11px; } -.fam-flag-gb { background-position: -176px -22px; } -.fam-flag-ga { background-position: -176px -33px; } -.fam-flag-fr { background-position: -176px -44px; } -.fam-flag-gf { background-position: -176px -44px; } -.fam-flag-re { background-position: -176px -44px; } -.fam-flag-mf { background-position: -176px -44px; } -.fam-flag-bl { background-position: -176px -44px; } -.fam-flag-fo { background-position: -176px -55px; } -.fam-flag-fm { background-position: -176px -66px; } -.fam-flag-fk { background-position: -176px -77px; } -.fam-flag-fj { background-position: -176px -88px; } -.fam-flag-fi { background-position: -176px -99px; } -.fam-flag-fam { background-position: -176px -110px; } -.fam-flag-eu { background-position: -176px -121px; } -.fam-flag-et { background-position: -176px -132px; } -.fam-flag-es { background-position: -176px -143px; } -.fam-flag-er { background-position: -176px -154px; } -.fam-flag-england { background-position: 0px -166px; } -.fam-flag-eh { background-position: -16px -166px; } -.fam-flag-eg { background-position: -32px -166px; } -.fam-flag-ee { background-position: -48px -166px; } -.fam-flag-ec { background-position: -64px -166px; } -.fam-flag-dz { background-position: -80px -166px; } -.fam-flag-do { background-position: -96px -166px; } -.fam-flag-dm { background-position: -112px -166px; } -.fam-flag-dk { background-position: -128px -166px; } -.fam-flag-dj { background-position: -144px -166px; } -.fam-flag-de { background-position: -160px -166px; } -.fam-flag-cz { background-position: -176px -166px; } -.fam-flag-cy { background-position: 0px -177px; } -.fam-flag-cx { background-position: -16px -177px; } -.fam-flag-cw { background-position: -32px -177px; } -.fam-flag-cv { background-position: -48px -177px; } -.fam-flag-cu { background-position: -64px -177px; } -.fam-flag-cs { background-position: -80px -177px; } -.fam-flag-cr { background-position: -96px -177px; } -.fam-flag-co { background-position: -112px -177px; } -.fam-flag-cn { background-position: -128px -177px; } -.fam-flag-cm { background-position: -144px -177px; } -.fam-flag-cl { background-position: -160px -177px; } -.fam-flag-ck { background-position: -176px -177px; } -.fam-flag-ci { background-position: -192px 0px; } -.fam-flag-cg { background-position: -192px -11px; } -.fam-flag-cf { background-position: -192px -22px; } -.fam-flag-cd { background-position: -192px -33px; } -.fam-flag-cc { background-position: -192px -44px; } -.fam-flag-catalonia { background-position: -192px -55px; } -.fam-flag-ca { background-position: -192px -66px; } -.fam-flag-bz { background-position: -192px -77px; } -.fam-flag-by { background-position: -192px -88px; } -.fam-flag-bw { background-position: -192px -99px; } -.fam-flag-bt { background-position: -192px -110px; } -.fam-flag-bs { background-position: -192px -121px; } -.fam-flag-br { background-position: -192px -132px; } -.fam-flag-bq { background-position: -192px -143px; } -.fam-flag-bo { background-position: -192px -154px; } -.fam-flag-bn { background-position: -192px -165px; } -.fam-flag-bm { background-position: -192px -176px; } -.fam-flag-bj { background-position: 0px -188px; } -.fam-flag-bi { background-position: -16px -188px; } -.fam-flag-bh { background-position: -32px -188px; } -.fam-flag-bg { background-position: -48px -188px; } -.fam-flag-bf { background-position: -64px -188px; } -.fam-flag-be { background-position: -80px -188px; } -.fam-flag-bd { background-position: -96px -188px; } -.fam-flag-bb { background-position: -112px -188px; } -.fam-flag-ba { background-position: -128px -188px; } -.fam-flag-az { background-position: -144px -188px; } -.fam-flag-ax { background-position: -160px -188px; } -.fam-flag-aw { background-position: -176px -188px; } -.fam-flag-au { background-position: -192px -188px; } -.fam-flag-hm { background-position: -192px -188px; } -.fam-flag-at { background-position: -208px 0px; } -.fam-flag-as { background-position: -208px -11px; } -.fam-flag-ar { background-position: -208px -22px; } -.fam-flag-ao { background-position: -208px -33px; } -.fam-flag-an { background-position: -208px -44px; } -.fam-flag-am { background-position: -208px -55px; } -.fam-flag-al { background-position: -208px -66px; } -.fam-flag-ai { background-position: -208px -77px; } -.fam-flag-ag { background-position: -208px -88px; } -.fam-flag-af { background-position: -208px -99px; } -.fam-flag-ae { background-position: -208px -110px; } -.fam-flag-ad { background-position: -208px -121px; } -.fam-flag-np { background-position: -208px -132px; width: 9px; } -.fam-flag-ch { background-position: -208px -143px; width: 11px; } diff --git a/sitestatic/famfamfam-flags.png b/sitestatic/famfamfam-flags.png deleted file mode 100644 index 953287b9..00000000 Binary files a/sitestatic/famfamfam-flags.png and /dev/null differ diff --git a/sitestatic/flags/fam.css b/sitestatic/flags/fam.css new file mode 100644 index 00000000..f4c047d0 --- /dev/null +++ b/sitestatic/flags/fam.css @@ -0,0 +1,270 @@ +/** + * fam flag icons CSS. + */ + +.fam-flag { + display: inline-block; + width: 16px; + height: 11px; + line-height: 11px; + background-image: url("fam.png"); + background-position: -208px -188px; + background-repeat: no-repeat; +} + +.fam-flag-zw { background-position: 0px 0px; } +.fam-flag-zm { background-position: -16px 0px; } +.fam-flag-za { background-position: 0px -11px; } +.fam-flag-yt { background-position: -16px -11px; } +.fam-flag-ye { background-position: -32px 0px; } +.fam-flag-ws { background-position: -32px -11px; } +.fam-flag-wf { background-position: 0px -22px; } +.fam-flag-wales { background-position: -16px -22px; } +.fam-flag-vu { background-position: -32px -22px; } +.fam-flag-vn { background-position: 0px -33px; } +.fam-flag-vi { background-position: -16px -33px; } +.fam-flag-vg { background-position: -32px -33px; } +.fam-flag-ve { background-position: -48px 0px; } +.fam-flag-vc { background-position: -48px -11px; } +.fam-flag-va { background-position: -48px -22px; } +.fam-flag-uz { background-position: -48px -33px; } +.fam-flag-uy { background-position: 0px -44px; } +.fam-flag-us { background-position: -16px -44px; } +.fam-flag-um { background-position: -16px -44px; } +.fam-flag-ug { background-position: -32px -44px; } +.fam-flag-ua { background-position: -48px -44px; } +.fam-flag-tz { background-position: -64px 0px; } +.fam-flag-tw { background-position: -64px -11px; } +.fam-flag-tv { background-position: -64px -22px; } +.fam-flag-tt { background-position: -64px -33px; } +.fam-flag-tr { background-position: -64px -44px; } +.fam-flag-to { background-position: 0px -55px; } +.fam-flag-tn { background-position: -16px -55px; } +.fam-flag-tm { background-position: -32px -55px; } +.fam-flag-tl { background-position: -48px -55px; } +.fam-flag-tk { background-position: -64px -55px; } +.fam-flag-tj { background-position: 0px -66px; } +.fam-flag-th { background-position: -16px -66px; } +.fam-flag-tg { background-position: -32px -66px; } +.fam-flag-tf { background-position: -48px -66px; } +.fam-flag-td { background-position: -64px -66px; } +.fam-flag-tc { background-position: -80px 0px; } +.fam-flag-sz { background-position: -80px -11px; } +.fam-flag-sy { background-position: -80px -22px; } +.fam-flag-sx { background-position: -80px -33px; } +.fam-flag-sv { background-position: -80px -44px; } +.fam-flag-st { background-position: -80px -55px; } +.fam-flag-ss { background-position: -80px -66px; } +.fam-flag-sr { background-position: 0px -77px; } +.fam-flag-so { background-position: -16px -77px; } +.fam-flag-sn { background-position: -32px -77px; } +.fam-flag-sm { background-position: -48px -77px; } +.fam-flag-sl { background-position: -64px -77px; } +.fam-flag-sk { background-position: -80px -77px; } +.fam-flag-si { background-position: -96px 0px; } +.fam-flag-sh { background-position: -96px -11px; } +.fam-flag-sg { background-position: -96px -22px; } +.fam-flag-se { background-position: -96px -33px; } +.fam-flag-sd { background-position: -96px -44px; } +.fam-flag-scotland { background-position: -96px -55px; } +.fam-flag-sc { background-position: -96px -66px; } +.fam-flag-sb { background-position: -96px -77px; } +.fam-flag-sa { background-position: 0px -88px; } +.fam-flag-rw { background-position: -16px -88px; } +.fam-flag-ru { background-position: -32px -88px; } +.fam-flag-rs { background-position: -48px -88px; } +.fam-flag-ro { background-position: -64px -88px; } +.fam-flag-qa { background-position: -80px -88px; } +.fam-flag-py { background-position: -96px -88px; } +.fam-flag-pw { background-position: 0px -99px; } +.fam-flag-pt { background-position: -16px -99px; } +.fam-flag-ps { background-position: -32px -99px; } +.fam-flag-pr { background-position: -48px -99px; } +.fam-flag-pn { background-position: -64px -99px; } +.fam-flag-pm { background-position: -80px -99px; } +.fam-flag-pl { background-position: -96px -99px; } +.fam-flag-pk { background-position: -112px 0px; } +.fam-flag-ph { background-position: -112px -11px; } +.fam-flag-pg { background-position: -112px -22px; } +.fam-flag-pf { background-position: -112px -33px; } +.fam-flag-pe { background-position: -112px -44px; } +.fam-flag-pa { background-position: -112px -55px; } +.fam-flag-om { background-position: -112px -66px; } +.fam-flag-nz { background-position: -112px -77px; } +.fam-flag-nu { background-position: -112px -88px; } +.fam-flag-nr { background-position: -112px -99px; } +.fam-flag-no { background-position: 0px -110px; } +.fam-flag-bv { background-position: 0px -110px; } +.fam-flag-sj { background-position: 0px -110px; } +.fam-flag-nl { background-position: -16px -110px; } +.fam-flag-ni { background-position: -32px -110px; } +.fam-flag-ng { background-position: -48px -110px; } +.fam-flag-nf { background-position: -64px -110px; } +.fam-flag-ne { background-position: -80px -110px; } +.fam-flag-nc { background-position: -96px -110px; } +.fam-flag-na { background-position: -112px -110px; } +.fam-flag-mz { background-position: -128px 0px; } +.fam-flag-my { background-position: -128px -11px; } +.fam-flag-mx { background-position: -128px -22px; } +.fam-flag-mw { background-position: -128px -33px; } +.fam-flag-mv { background-position: -128px -44px; } +.fam-flag-mu { background-position: -128px -55px; } +.fam-flag-mt { background-position: -128px -66px; } +.fam-flag-ms { background-position: -128px -77px; } +.fam-flag-mr { background-position: -128px -88px; } +.fam-flag-mq { background-position: -128px -99px; } +.fam-flag-mp { background-position: -128px -110px; } +.fam-flag-mo { background-position: 0px -121px; } +.fam-flag-mn { background-position: -16px -121px; } +.fam-flag-mm { background-position: -32px -121px; } +.fam-flag-ml { background-position: -48px -121px; } +.fam-flag-mk { background-position: -64px -121px; } +.fam-flag-mh { background-position: -80px -121px; } +.fam-flag-mg { background-position: -96px -121px; } +.fam-flag-me { background-position: 0px -132px; width: 16px; height: 12px; } +.fam-flag-md { background-position: -112px -121px; } +.fam-flag-mc { background-position: -128px -121px; } +.fam-flag-ma { background-position: -16px -132px; } +.fam-flag-ly { background-position: -32px -132px; } +.fam-flag-lv { background-position: -48px -132px; } +.fam-flag-lu { background-position: -64px -132px; } +.fam-flag-lt { background-position: -80px -132px; } +.fam-flag-ls { background-position: -96px -132px; } +.fam-flag-lr { background-position: -112px -132px; } +.fam-flag-lk { background-position: -128px -132px; } +.fam-flag-li { background-position: -144px 0px; } +.fam-flag-lc { background-position: -144px -11px; } +.fam-flag-lb { background-position: -144px -22px; } +.fam-flag-la { background-position: -144px -33px; } +.fam-flag-kz { background-position: -144px -44px; } +.fam-flag-ky { background-position: -144px -55px; } +.fam-flag-kw { background-position: -144px -66px; } +.fam-flag-kr { background-position: -144px -77px; } +.fam-flag-kp { background-position: -144px -88px; } +.fam-flag-kn { background-position: -144px -99px; } +.fam-flag-km { background-position: -144px -110px; } +.fam-flag-ki { background-position: -144px -121px; } +.fam-flag-kh { background-position: -144px -132px; } +.fam-flag-kg { background-position: 0px -144px; } +.fam-flag-ke { background-position: -16px -144px; } +.fam-flag-jp { background-position: -32px -144px; } +.fam-flag-jo { background-position: -48px -144px; } +.fam-flag-jm { background-position: -64px -144px; } +.fam-flag-je { background-position: -80px -144px; } +.fam-flag-it { background-position: -96px -144px; } +.fam-flag-is { background-position: -112px -144px; } +.fam-flag-ir { background-position: -128px -144px; } +.fam-flag-iq { background-position: -144px -144px; } +.fam-flag-io { background-position: -160px 0px; } +.fam-flag-in { background-position: -160px -11px; } +.fam-flag-im { background-position: -160px -22px; width: 16px; height: 9px; } +.fam-flag-il { background-position: -160px -31px; } +.fam-flag-ie { background-position: -160px -42px; } +.fam-flag-id { background-position: -160px -53px; } +.fam-flag-hu { background-position: -160px -64px; } +.fam-flag-ht { background-position: -160px -75px; } +.fam-flag-hr { background-position: -160px -86px; } +.fam-flag-hn { background-position: -160px -97px; } +.fam-flag-hk { background-position: -160px -108px; } +.fam-flag-gy { background-position: -160px -119px; } +.fam-flag-gw { background-position: -160px -130px; } +.fam-flag-gu { background-position: -160px -141px; } +.fam-flag-gt { background-position: 0px -155px; } +.fam-flag-gs { background-position: -16px -155px; } +.fam-flag-gr { background-position: -32px -155px; } +.fam-flag-gq { background-position: -48px -155px; } +.fam-flag-gp { background-position: -64px -155px; } +.fam-flag-gn { background-position: -80px -155px; } +.fam-flag-gm { background-position: -96px -155px; } +.fam-flag-gl { background-position: -112px -155px; } +.fam-flag-gi { background-position: -128px -155px; } +.fam-flag-gh { background-position: -144px -155px; } +.fam-flag-gg { background-position: -160px -155px; } +.fam-flag-ge { background-position: -176px 0px; } +.fam-flag-gd { background-position: -176px -11px; } +.fam-flag-gb { background-position: -176px -22px; } +.fam-flag-ga { background-position: -176px -33px; } +.fam-flag-fr { background-position: -176px -44px; } +.fam-flag-gf { background-position: -176px -44px; } +.fam-flag-re { background-position: -176px -44px; } +.fam-flag-mf { background-position: -176px -44px; } +.fam-flag-bl { background-position: -176px -44px; } +.fam-flag-fo { background-position: -176px -55px; } +.fam-flag-fm { background-position: -176px -66px; } +.fam-flag-fk { background-position: -176px -77px; } +.fam-flag-fj { background-position: -176px -88px; } +.fam-flag-fi { background-position: -176px -99px; } +.fam-flag-fam { background-position: -176px -110px; } +.fam-flag-eu { background-position: -176px -121px; } +.fam-flag-et { background-position: -176px -132px; } +.fam-flag-es { background-position: -176px -143px; } +.fam-flag-er { background-position: -176px -154px; } +.fam-flag-england { background-position: 0px -166px; } +.fam-flag-eh { background-position: -16px -166px; } +.fam-flag-eg { background-position: -32px -166px; } +.fam-flag-ee { background-position: -48px -166px; } +.fam-flag-ec { background-position: -64px -166px; } +.fam-flag-dz { background-position: -80px -166px; } +.fam-flag-do { background-position: -96px -166px; } +.fam-flag-dm { background-position: -112px -166px; } +.fam-flag-dk { background-position: -128px -166px; } +.fam-flag-dj { background-position: -144px -166px; } +.fam-flag-de { background-position: -160px -166px; } +.fam-flag-cz { background-position: -176px -166px; } +.fam-flag-cy { background-position: 0px -177px; } +.fam-flag-cx { background-position: -16px -177px; } +.fam-flag-cw { background-position: -32px -177px; } +.fam-flag-cv { background-position: -48px -177px; } +.fam-flag-cu { background-position: -64px -177px; } +.fam-flag-cs { background-position: -80px -177px; } +.fam-flag-cr { background-position: -96px -177px; } +.fam-flag-co { background-position: -112px -177px; } +.fam-flag-cn { background-position: -128px -177px; } +.fam-flag-cm { background-position: -144px -177px; } +.fam-flag-cl { background-position: -160px -177px; } +.fam-flag-ck { background-position: -176px -177px; } +.fam-flag-ci { background-position: -192px 0px; } +.fam-flag-cg { background-position: -192px -11px; } +.fam-flag-cf { background-position: -192px -22px; } +.fam-flag-cd { background-position: -192px -33px; } +.fam-flag-cc { background-position: -192px -44px; } +.fam-flag-catalonia { background-position: -192px -55px; } +.fam-flag-ca { background-position: -192px -66px; } +.fam-flag-bz { background-position: -192px -77px; } +.fam-flag-by { background-position: -192px -88px; } +.fam-flag-bw { background-position: -192px -99px; } +.fam-flag-bt { background-position: -192px -110px; } +.fam-flag-bs { background-position: -192px -121px; } +.fam-flag-br { background-position: -192px -132px; } +.fam-flag-bq { background-position: -192px -143px; } +.fam-flag-bo { background-position: -192px -154px; } +.fam-flag-bn { background-position: -192px -165px; } +.fam-flag-bm { background-position: -192px -176px; } +.fam-flag-bj { background-position: 0px -188px; } +.fam-flag-bi { background-position: -16px -188px; } +.fam-flag-bh { background-position: -32px -188px; } +.fam-flag-bg { background-position: -48px -188px; } +.fam-flag-bf { background-position: -64px -188px; } +.fam-flag-be { background-position: -80px -188px; } +.fam-flag-bd { background-position: -96px -188px; } +.fam-flag-bb { background-position: -112px -188px; } +.fam-flag-ba { background-position: -128px -188px; } +.fam-flag-az { background-position: -144px -188px; } +.fam-flag-ax { background-position: -160px -188px; } +.fam-flag-aw { background-position: -176px -188px; } +.fam-flag-au { background-position: -192px -188px; } +.fam-flag-hm { background-position: -192px -188px; } +.fam-flag-at { background-position: -208px 0px; } +.fam-flag-as { background-position: -208px -11px; } +.fam-flag-ar { background-position: -208px -22px; } +.fam-flag-ao { background-position: -208px -33px; } +.fam-flag-an { background-position: -208px -44px; } +.fam-flag-am { background-position: -208px -55px; } +.fam-flag-al { background-position: -208px -66px; } +.fam-flag-ai { background-position: -208px -77px; } +.fam-flag-ag { background-position: -208px -88px; } +.fam-flag-af { background-position: -208px -99px; } +.fam-flag-ae { background-position: -208px -110px; } +.fam-flag-ad { background-position: -208px -121px; } +.fam-flag-np { background-position: -208px -132px; width: 9px; } +.fam-flag-ch { background-position: -208px -143px; width: 11px; } diff --git a/sitestatic/flags/fam.png b/sitestatic/flags/fam.png new file mode 100644 index 00000000..953287b9 Binary files /dev/null and b/sitestatic/flags/fam.png differ -- cgit v1.2.3-54-g00ecf From 1f9aef78f39c90191eddf2233c278086a15052de Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 16 Jan 2013 00:18:26 -0600 Subject: Remove debugging print statement Signed-off-by: Dan McGee --- mirrors/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mirrors/views.py b/mirrors/views.py index c0ed6670..56397633 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -181,7 +181,6 @@ def mirror_details(request, name): all_urls = set(mirror.urls.select_related('protocol')) # Add dummy data for URLs that we haven't checked recently other_urls = all_urls.difference(checked_urls) - print other_urls for url in other_urls: for attr in ('last_sync', 'completion_pct', 'delay', 'duration_avg', 'duration_stddev', 'score'): -- cgit v1.2.3-54-g00ecf From 0b930fd92140858f4ad21e593feb057996af9b95 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 16 Jan 2013 00:36:17 -0600 Subject: Convert all usages of flag icons to new sprite This uses a new template tag to avoid repeating construction of the necessary HTML element all over the place. The site should look exactly as it did before, except now you don't have to download 20+ images to see some pages. Signed-off-by: Dan McGee --- main/templatetags/flags.py | 13 +++++++++++++ templates/devel/clock.html | 5 ++++- templates/mirrors/mirror_details.html | 5 ++++- templates/mirrors/status.html | 5 ++++- templates/mirrors/status_table.html | 3 ++- templates/public/developer_list.html | 3 ++- templates/public/download.html | 6 +++++- templates/public/userlist.html | 3 +++ 8 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 main/templatetags/flags.py diff --git a/main/templatetags/flags.py b/main/templatetags/flags.py new file mode 100644 index 00000000..22f524ca --- /dev/null +++ b/main/templatetags/flags.py @@ -0,0 +1,13 @@ +from django import template + +register = template.Library() + + +@register.simple_tag +def country_flag(country): + if not country: + return '' + return ' ' % ( + country.code.lower(), country.name) + +# vim: set ts=4 sw=4 et: diff --git a/templates/devel/clock.html b/templates/devel/clock.html index 02e42749..83fbb70b 100644 --- a/templates/devel/clock.html +++ b/templates/devel/clock.html @@ -1,9 +1,12 @@ {% extends "base.html" %} {% load static from staticfiles %} +{% load flags %} {% load tz %} {% block title %}Arch Linux - Developer World Clocks{% endblock %} +{% block head %}{% endblock %} + {% block content %}

    Developer World Clocks

    @@ -45,7 +48,7 @@

    Developer World Clocks

    {{ dev.username }} {{ dev.userprofile.alias }} {{ dev.last_action }} - {% if dev.userprofile.country %}{{ dev.userprofile.country.name }} {% endif %}{{ dev.userprofile.location }} + {% country_flag dev.userprofile.country %}{{ dev.userprofile.location }} {{ dev.userprofile.time_zone }} {{ utc_now|timezone:dev.userprofile.time_zone|date:"Y-m-d H:i T" }} {{ dev.userprofile.time_zone }} diff --git a/templates/mirrors/mirror_details.html b/templates/mirrors/mirror_details.html index a56123ff..8ea6bbec 100644 --- a/templates/mirrors/mirror_details.html +++ b/templates/mirrors/mirror_details.html @@ -1,9 +1,12 @@ {% extends "base.html" %} {% load static from staticfiles %} {% load mirror_status %} +{% load flags %} {% block title %}Arch Linux - {{ mirror.name }} - Mirror Details{% endblock %} +{% block head %}{% endblock %} + {% block content %}
    @@ -90,7 +93,7 @@

    Available URLs

    {% if m_url.protocol.is_download %}{{ m_url.url }}{% else %}{{ m_url.url }}{% endif %} {{ m_url.protocol }} - {% if m_url.country %} {% endif %}{{ m_url.country.name }} + {% country_flag m_url.country %}{{ m_url.country.name }} {{ m_url.has_ipv4|yesno|capfirst }} {{ m_url.has_ipv6|yesno|capfirst }} {{ m_url.last_sync|date:'Y-m-d H:i'|default:'unknown' }} diff --git a/templates/mirrors/status.html b/templates/mirrors/status.html index ec2ae568..8d32d3fa 100644 --- a/templates/mirrors/status.html +++ b/templates/mirrors/status.html @@ -1,9 +1,12 @@ {% extends "base.html" %} {% load static from staticfiles %} {% load mirror_status %} +{% load flags %} {% block title %}Arch Linux - Mirror Status{% if tier != None %} - Tier {{ tier }}{% endif %}{% endblock %} +{% block head %}{% endblock %} + {% block content %}

    Mirror Status{% if tier != None %} - Tier {{ tier }}{% endif %}

    @@ -88,7 +91,7 @@

    Mirror Syncing Error Log

    {% spaceless %} {{ log.url__url }} {{ log.url__protocol__protocol }} - {% if log.country %} {% endif %}{{ log.country.name }} + {% country_flag log.country %}{{ log.country.name }} {{ log.error|linebreaksbr }} {{ log.last_occurred|date:'Y-m-d H:i' }} {{ log.error_count }} diff --git a/templates/mirrors/status_table.html b/templates/mirrors/status_table.html index c7394de6..2dd7ef49 100644 --- a/templates/mirrors/status_table.html +++ b/templates/mirrors/status_table.html @@ -1,4 +1,5 @@ {% load mirror_status %} +{% load flags %} @@ -17,7 +18,7 @@ {% spaceless %} - + diff --git a/templates/public/developer_list.html b/templates/public/developer_list.html index df4137eb..4401d97b 100644 --- a/templates/public/developer_list.html +++ b/templates/public/developer_list.html @@ -1,3 +1,4 @@ +{% load flags %} {% load pgp %}
    @@ -56,7 +57,7 @@

    {{ dev.get_full_name }}{% if prof.latin_name %} ({{ prof.latin_name}}){% end

    - + diff --git a/templates/public/download.html b/templates/public/download.html index 0c96fcef..7de49778 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -2,8 +2,12 @@ {% load cache %} {% load url from future %} {% load static from staticfiles %} +{% load flags %} {% block title %}Arch Linux - Downloads{% endblock %} + +{% block head %}{% endblock %} + {% block navbarclass %}anb-download{% endblock %} {% block content %} @@ -83,7 +87,7 @@

    Checksums

    {% regroup mirror_urls by country as grouped_urls %} {% for country in grouped_urls %} - {% if country.grouper %}
    {{ country.grouper.name }}
    + {% if country.grouper %}
    {% country_flag country.grouper %}{{ country.grouper.name }}
    {% else %}
    Worldwide
    {% endif %}
      {% for mirror_url in country.list %} diff --git a/templates/public/userlist.html b/templates/public/userlist.html index 0077f611..35104317 100644 --- a/templates/public/userlist.html +++ b/templates/public/userlist.html @@ -1,8 +1,11 @@ {% extends "base.html" %} +{% load static from staticfiles %} {% load cache %} {% block title %}Arch Linux - {{ user_type }}{% endblock %} +{% block head %}{% endblock %} + {% block content %} {% cache 600 dev-tu-profiles user_type %}
      -- cgit v1.2.3-54-g00ecf From cba22a378b48f1a4f185a56a21f39483595cf8a6 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 Jan 2013 20:01:22 -0600 Subject: Don't pull and sort mirror URLs unless we have to On the download page, the explicit sorted() call was forcing evaluation of the Django queryset, even if we never actually needed the results because the template fragment was cached. Wrap it all in a callable function which looks the same to the template, but saves us the cost of evaluation every single page view. Signed-off-by: Dan McGee --- public/views.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/public/views.py b/public/views.py index e15f5b89..3c823c9d 100644 --- a/public/views.py +++ b/public/views.py @@ -82,11 +82,16 @@ def download(request): release = Release.objects.filter(available=True).latest() except Release.DoesNotExist: release = None - mirror_urls = MirrorUrl.objects.select_related('mirror').filter( - protocol__default=True, - mirror__public=True, mirror__active=True, mirror__isos=True) - sort_by = attrgetter('country.name', 'mirror.name') - mirror_urls = sorted(mirror_urls, key=sort_by) + + def mirror_urls(): + '''In order to ensure this is lazily evaluated since we can't do + sorting at the database level, make it a callable.''' + urls = MirrorUrl.objects.select_related('mirror').filter( + protocol__default=True, + mirror__public=True, mirror__active=True, mirror__isos=True) + sort_by = attrgetter('country.name', 'mirror.name') + return sorted(urls, key=sort_by) + context = { 'release': release, 'releng_iso_url': settings.ISO_LIST_URL, -- cgit v1.2.3-54-g00ecf From 7313a7914dac9c86d48656964d310cff7fc2a0e1 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 Jan 2013 20:15:40 -0600 Subject: Include 'files_last_update' in package sitemap query We need this for the package files sitemap; else we are doing one query per package to retrieve this field. Whoops. Signed-off-by: Dan McGee --- sitemaps.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sitemaps.py b/sitemaps.py index 0388627b..110c588a 100644 --- a/sitemaps.py +++ b/sitemaps.py @@ -14,7 +14,8 @@ class PackagesSitemap(Sitemap): def items(self): return Package.objects.normal().filter(repo__staging=False).only( - 'pkgname', 'last_update', 'repo__name', 'arch__name').order_by() + 'pkgname', 'last_update', 'files_last_update', + 'repo__name', 'arch__name').order_by() def lastmod(self, obj): return obj.last_update -- cgit v1.2.3-54-g00ecf From 375684ed91dd5499e7a4ea7787e45803e8467e16 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 18 Jan 2013 20:52:20 -0600 Subject: Use a set instead of list when gathering package differences If we implement the __eq__ and __hash__ methods, we can use a set to gather package difference objects and make deduplication of objects a lot more efficient. Signed-off-by: Dan McGee --- packages/utils.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/utils.py b/packages/utils.py index 5f0c111e..a72404f4 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -1,6 +1,6 @@ from collections import defaultdict from itertools import chain -from operator import itemgetter +from operator import attrgetter, itemgetter import re from django.core.serializers.json import DjangoJSONEncoder @@ -108,10 +108,15 @@ def classes(self): css_classes.append(self.pkg_b.arch.name) return ' '.join(css_classes) - def __cmp__(self, other): - if isinstance(other, Difference): - return cmp(self.__dict__, other.__dict__) - return False + def __key(self): + return (self.pkgname, hash(self.repo), + hash(self.pkg_a), hash(self.pkg_b)) + + def __eq__(self, other): + return self.__key() == other.__key() + + def __hash__(self): + return hash(self.__key()) @cache_function(127) @@ -146,8 +151,8 @@ def get_differences_info(arch_a, arch_b): to_fetch = {row[0] for row in results} # fetch all of the necessary packages pkgs = Package.objects.normal().in_bulk(to_fetch) - # now build a list of tuples containing differences - differences = [] + # now build a set containing differences + differences = set() for row in results: pkg_a = pkgs.get(row[0]) pkg_b = pkgs.get(row[1]) @@ -160,11 +165,11 @@ def get_differences_info(arch_a, arch_b): name = pkg_a.pkgname if pkg_a else pkg_b.pkgname repo = pkg_a.repo if pkg_a else pkg_b.repo item = Difference(name, repo, pkg_b, pkg_a) - if item not in differences: - differences.append(item) + differences.add(item) # now sort our list by repository, package name - differences.sort(key=lambda a: (a.repo.name, a.pkgname)) + key_func = attrgetter('repo.name', 'pkgname') + differences = sorted(differences, key=key_func) return differences -- cgit v1.2.3-54-g00ecf From a42a0dc6e400d03609d2d53c0955273b3c05c7ea Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 09:48:15 -0600 Subject: Add double click event handler to hide image Signed-off-by: Dan McGee --- templates/public/index.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/templates/public/index.html b/templates/public/index.html index abf16afd..6fc90436 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -240,6 +240,9 @@

      More Resources

      setTimeout(function() { $('#konami').fadeIn(500); }, 500); + $('#konami').click(function() { + $('#konami').fadeOut(500); + }); }; konami.iphone.code = konami.code; konami.load(); -- cgit v1.2.3-54-g00ecf From fbc153ed456f2f4aa730580f942d10d973fd1ea9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 10:33:03 -0600 Subject: Initial cut of a Releases RSS feed This is our first use of enclosures on the site as well. Signed-off-by: Dan McGee --- feeds.py | 43 +++++++++++++++++++++++++++++++++++++++++++ urls.py | 3 ++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/feeds.py b/feeds.py index c5bdbdee..98e1fb82 100644 --- a/feeds.py +++ b/feeds.py @@ -1,4 +1,6 @@ +from datetime import datetime, time import hashlib +from pytz import utc from django.contrib.sites.models import Site from django.contrib.syndication.views import Feed @@ -9,6 +11,7 @@ from main.utils import retrieve_latest from main.models import Arch, Repo, Package from news.models import News +from releng.models import Release class GuidNotPermalinkFeed(Rss201rev2Feed): @@ -152,4 +155,44 @@ def item_pubdate(self, item): def item_author_name(self, item): return item.author.get_full_name() + +class ReleaseFeed(Feed): + feed_type = GuidNotPermalinkFeed + + title = 'Arch Linux: Releases' + link = '/download/' + description = 'Release ISOs' + subtitle = description + + __name__ = 'release_feed' + + def items(self): + return Release.objects.filter(available=True)[:10] + + def item_title(self, item): + return item.version + + def item_description(self, item): + return item.info_html() + + # TODO: individual release pages + item_link = '/download/' + + def item_pubdate(self, item): + return datetime.combine(item.release_date, time()).replace(tzinfo=utc) + + def item_guid(self, item): + # http://diveintomark.org/archives/2004/05/28/howto-atom-id + date = item.release_date + return 'tag:%s,%s:%s' % (Site.objects.get_current().domain, + date.strftime('%Y-%m-%d'), item.iso_url()) + + def item_enclosure_url(self, item): + domain = Site.objects.get_current().domain + proto = 'https' + return "%s://%s/%s.torrent" % (proto, domain, item.iso_url()) + + item_enclosure_mime_type = 'application/x-bittorrent' + item_enclosure_length = 0 + # vim: set ts=4 sw=4 et: diff --git a/urls.py b/urls.py index 1bdf1a58..82882970 100644 --- a/urls.py +++ b/urls.py @@ -6,7 +6,7 @@ from django.views.generic import TemplateView, RedirectView from django.views.i18n import null_javascript_catalog -from feeds import PackageFeed, NewsFeed +from feeds import PackageFeed, NewsFeed, ReleaseFeed import sitemaps our_sitemaps = { @@ -32,6 +32,7 @@ cache_page(300)(PackageFeed())), (r'^packages/(?P[A-z0-9]+)/(?P[A-z0-9\-]+)/$', cache_page(300)(PackageFeed())), + (r'^releases/$', cache_page(300)(ReleaseFeed())), ) # Sitemaps -- cgit v1.2.3-54-g00ecf From 721f092e4a36a58da2f701973521fb087a977693 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 10:36:49 -0600 Subject: Publicize the releases feed Signed-off-by: Dan McGee --- templates/public/download.html | 5 ++++- templates/public/feeds.html | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/templates/public/download.html b/templates/public/download.html index 7de49778..8ddbc23e 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -6,7 +6,10 @@ {% block title %}Arch Linux - Downloads{% endblock %} -{% block head %}{% endblock %} +{% block head %} + + +{% endblock %} {% block navbarclass %}anb-download{% endblock %} diff --git a/templates/public/feeds.html b/templates/public/feeds.html index 4a24b8d5..f354fa47 100644 --- a/templates/public/feeds.html +++ b/templates/public/feeds.html @@ -12,7 +12,7 @@

      RSS Feeds

      News and Activity Feeds

      -

      Grab the news item feed +

      Grab the news item feed to keep up-to-date with the latest news from the Arch Linux development staff.

      The Package Feeds

      A newest packages feed is also available from the Arch User Repository (AUR).

      +

      Release Feed

      + +

      Grab the ISO release feed + if you want to help seed the ISO release torrents as they come out.

      +

      Development Feeds

      Subscribe to any of the following to track bug tickets and feature -- cgit v1.2.3-54-g00ecf From 23ef118ac129e17d251634d2ef3c88c6d74279e3 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 10:37:14 -0600 Subject: Don't redefine mirror_url function every call If we pull this out and define it at the top level once, we save the interpreter a fair amount of work. Signed-off-by: Dan McGee --- public/views.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/public/views.py b/public/views.py index 3c823c9d..22cb8759 100644 --- a/public/views.py +++ b/public/views.py @@ -76,6 +76,16 @@ def donate(request): return render(request, 'public/donate.html', context) +def _mirror_urls(): + '''In order to ensure this is lazily evaluated since we can't do + sorting at the database level, make it a callable.''' + urls = MirrorUrl.objects.select_related('mirror').filter( + protocol__default=True, + mirror__public=True, mirror__active=True, mirror__isos=True) + sort_by = attrgetter('country.name', 'mirror.name') + return sorted(urls, key=sort_by) + + @cache_control(max_age=300) def download(request): try: @@ -83,20 +93,11 @@ def download(request): except Release.DoesNotExist: release = None - def mirror_urls(): - '''In order to ensure this is lazily evaluated since we can't do - sorting at the database level, make it a callable.''' - urls = MirrorUrl.objects.select_related('mirror').filter( - protocol__default=True, - mirror__public=True, mirror__active=True, mirror__isos=True) - sort_by = attrgetter('country.name', 'mirror.name') - return sorted(urls, key=sort_by) - context = { 'release': release, 'releng_iso_url': settings.ISO_LIST_URL, 'releng_pxeboot_url': settings.PXEBOOT_URL, - 'mirror_urls': mirror_urls, + 'mirror_urls': _mirror_urls, } return render(request, 'public/download.html', context) -- cgit v1.2.3-54-g00ecf From 8cfaa39a0464f7ee35af76473d3e73ae587a6cb8 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 10:55:58 -0600 Subject: Add more metadata to releng Release model Add a file_size field which we will use in the RSS feed, and also add a field for future storage of the torrent data itself. Signed-off-by: Dan McGee --- feeds.py | 4 +- ...se_file_size__add_field_release_torrent_data.py | 127 +++++++++++++++++++++ releng/models.py | 3 + templates/public/download.html | 4 +- 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 releng/migrations/0005_auto__add_field_release_file_size__add_field_release_torrent_data.py diff --git a/feeds.py b/feeds.py index 98e1fb82..c68d3b0e 100644 --- a/feeds.py +++ b/feeds.py @@ -192,7 +192,9 @@ def item_enclosure_url(self, item): proto = 'https' return "%s://%s/%s.torrent" % (proto, domain, item.iso_url()) + def item_enclosure_length(self, item): + return item.file_size or "" + item_enclosure_mime_type = 'application/x-bittorrent' - item_enclosure_length = 0 # vim: set ts=4 sw=4 et: diff --git a/releng/migrations/0005_auto__add_field_release_file_size__add_field_release_torrent_data.py b/releng/migrations/0005_auto__add_field_release_file_size__add_field_release_torrent_data.py new file mode 100644 index 00000000..96e0727c --- /dev/null +++ b/releng/migrations/0005_auto__add_field_release_file_size__add_field_release_torrent_data.py @@ -0,0 +1,127 @@ +# -*- coding: 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 'Release.file_size' + db.add_column('releng_release', 'file_size', + self.gf('main.fields.PositiveBigIntegerField')(null=True, blank=True), + keep_default=False) + + # Adding field 'Release.torrent_data' + db.add_column('releng_release', 'torrent_data', + self.gf('django.db.models.fields.TextField')(default='', blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Release.file_size' + db.delete_column('releng_release', 'file_size') + + # Deleting field 'Release.torrent_data' + db.delete_column('releng_release', 'torrent_data') + + + 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'}), + 'removed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}) + }, + '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.release': { + 'Meta': {'ordering': "('-release_date', '-version')", 'object_name': 'Release'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'file_size': ('main.fields.PositiveBigIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'kernel_version': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'release_date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}), + 'torrent_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'torrent_infohash': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + '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.GenericIPAddressField', [], {'max_length': '39'}), + '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'] \ No newline at end of file diff --git a/releng/models.py b/releng/models.py index a22e01d6..9f091371 100644 --- a/releng/models.py +++ b/releng/models.py @@ -5,6 +5,7 @@ from django.db.models.signals import pre_save from django.utils.safestring import mark_safe +from main.fields import PositiveBigIntegerField from main.utils import set_created_field @@ -113,9 +114,11 @@ class Release(models.Model): version = models.CharField(max_length=50) kernel_version = models.CharField(max_length=50, blank=True) torrent_infohash = models.CharField(max_length=64, blank=True) + file_size = PositiveBigIntegerField(null=True, blank=True) created = models.DateTimeField(editable=False) available = models.BooleanField(default=True) info = models.TextField('Public information', blank=True) + torrent_data = models.TextField(blank=True) class Meta: get_latest_by = 'release_date' diff --git a/templates/public/download.html b/templates/public/download.html index 8ddbc23e..29490849 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -58,7 +58,9 @@

      BitTorrent Download (recommended)

      A web-seed capable client is recommended for fastest download speeds.

      Download torrent for {{ release.version }} - (Magnet)

      + (Magnet) + {% if release.file_size %}({{ release.file_size|filesizeformat }}){% endif %} +

      Netboot

      -- cgit v1.2.3-54-g00ecf From 1a7e5d22f1c4e948c624d26b4d8c1ed30189acfe Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 12:06:47 -0600 Subject: Add basic release list and details views Signed-off-by: Dan McGee --- feeds.py | 5 +--- releng/models.py | 3 +++ releng/urls.py | 6 +++++ releng/views.py | 13 ++++++++++- templates/releng/release_detail.html | 24 ++++++++++++++++++++ templates/releng/release_list.html | 44 ++++++++++++++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 templates/releng/release_detail.html create mode 100644 templates/releng/release_list.html diff --git a/feeds.py b/feeds.py index c68d3b0e..9721f41c 100644 --- a/feeds.py +++ b/feeds.py @@ -175,9 +175,6 @@ def item_title(self, item): def item_description(self, item): return item.info_html() - # TODO: individual release pages - item_link = '/download/' - def item_pubdate(self, item): return datetime.combine(item.release_date, time()).replace(tzinfo=utc) @@ -185,7 +182,7 @@ def item_guid(self, item): # http://diveintomark.org/archives/2004/05/28/howto-atom-id date = item.release_date return 'tag:%s,%s:%s' % (Site.objects.get_current().domain, - date.strftime('%Y-%m-%d'), item.iso_url()) + date.strftime('%Y-%m-%d'), item.get_absolute_url()) def item_enclosure_url(self, item): domain = Site.objects.get_current().domain diff --git a/releng/models.py b/releng/models.py index 9f091371..8bc54def 100644 --- a/releng/models.py +++ b/releng/models.py @@ -127,6 +127,9 @@ class Meta: def __unicode__(self): return self.version + def get_absolute_url(self): + return reverse('releng-release-detail', args=[self.version]) + def dir_path(self): return "iso/%s/" % self.version diff --git a/releng/urls.py b/releng/urls.py index 8d1b8f24..8413d318 100644 --- a/releng/urls.py +++ b/releng/urls.py @@ -1,5 +1,7 @@ from django.conf.urls import include, patterns +from .views import ReleaseListView, ReleaseDetailView + feedback_patterns = patterns('releng.views', (r'^$', 'test_results_overview', {}, 'releng-test-overview'), (r'^submit/$', 'submit_test_result', {}, 'releng-test-submit'), @@ -11,5 +13,9 @@ urlpatterns = patterns('', (r'^feedback/', include(feedback_patterns)), + (r'^releases/$', + ReleaseListView.as_view(), {}, 'releng-release-list'), + (r'^releases/(?P[-.\w]+)/$', + ReleaseDetailView.as_view(), {}, 'releng-release-detail'), ) # vim: set ts=4 sw=4 et: diff --git a/releng/views.py b/releng/views.py index 67b3cb4a..6c49275f 100644 --- a/releng/views.py +++ b/releng/views.py @@ -3,10 +3,11 @@ from django.db.models import Count, Max from django.http import Http404 from django.shortcuts import get_object_or_404, redirect, render +from django.views.generic import DetailView, ListView from .models import (Architecture, BootType, Bootloader, ClockChoice, Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source, - Test) + Test, Release) def standard_field(model, empty_label=None, help_text=None, required=True): @@ -213,4 +214,14 @@ def iso_overview(request): } return render(request, 'releng/iso_overview.html', context) + +class ReleaseListView(ListView): + model = Release + + +class ReleaseDetailView(DetailView): + model = Release + slug_field = 'version' + slug_url_kwarg = 'version' + # vim: set ts=4 sw=4 et: diff --git a/templates/releng/release_detail.html b/templates/releng/release_detail.html new file mode 100644 index 00000000..fec9ce2b --- /dev/null +++ b/templates/releng/release_detail.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% block title %}Arch Linux - Release: {{ release.version }}{% endblock %} + +{% block content %} +
      +

      {{ release.version }}

      + +
        +
      • Release Date: {{ release.release_date|date }}
      • + {% if release.kernel_version %}
      • Kernel Version: {{ release.kernel_version }}
      • {% endif %} +
      • Available: {{ release.available|yesno }}
      • + {% if release.available %}
      • Torrent
      • {% endif %} + {% if release.available %}
      • Magnet
      • {% endif %} +
      • Download Size: {% if release.file_size %}{{ release.file_size|filesizeformat }}{% else %}Unknown{% endif %}
      • +
      + + {% if release.info %} +

      Release Notes

      + +
      {{ release.info_html }}
      + {% endif %} +
      +{% endblock %} diff --git a/templates/releng/release_list.html b/templates/releng/release_list.html new file mode 100644 index 00000000..1657249f --- /dev/null +++ b/templates/releng/release_list.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% load url from future %} + +{% block title %}Arch Linux - Releases{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
      + +

      Releases

      + +
    {{ m_url.url }} {{ m_url.protocol }}{% if m_url.country %} {% endif %}{{ m_url.country.name }}{% country_flag m_url.country %}{{ m_url.country.name }} {{ m_url.completion_pct|percentage:1 }} {{ m_url.delay|duration|default:'unknown' }} {{ m_url.duration_avg|floatformat:2 }}{% if prof.yob %}{{ prof.yob }}{% endif %}
    Location:{% if dev.userprofile.country %}{{ dev.userprofile.country.name }} {% endif %}{{ prof.location }}{% country_flag dev.userprofile.country %}{{ prof.location }}
    Languages: {{ prof.languages }}
    + + + + + + + + + + + + + {% for item in release_list %} + + + + + + + + + + {% endfor %} + +
    Release DateVersionKernel VersionAvailable?TorrentMagnetDownload Size
    {{ item.release_date|date }}{{ item.version }}{{ item.kernel_version|default:"" }}{{ item.available|yesno }}{% if item.available %}Torrent{% endif %}{% if item.available %}Magnet{% endif %}{% if item.file_size %}{{ item.file_size|filesizeformat }}{% endif %}
    + +
    +{% endblock %} -- cgit v1.2.3-54-g00ecf From 71c0c7453a5bc28b6e6576fe4f1351139f33ade5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 13:08:06 -0600 Subject: Implement torrent data parsing and extraction via bencode This allows uploading of the actual torrent file itself into the webapp and then pulling the relevant pieces of information out of it. Signed-off-by: Dan McGee --- releng/models.py | 33 +++++++++++++++++++++++++++++++++ requirements.txt | 3 ++- requirements_prod.txt | 3 ++- templates/releng/release_detail.html | 24 +++++++++++++++++++++--- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/releng/models.py b/releng/models.py index 8bc54def..c7d26966 100644 --- a/releng/models.py +++ b/releng/models.py @@ -1,4 +1,9 @@ +from base64 import b64decode +from bencode import bdecode, bencode +from datetime import datetime +import hashlib import markdown +from pytz import utc from django.core.urlresolvers import reverse from django.db import models @@ -150,6 +155,34 @@ def info_html(self): return mark_safe(markdown.markdown( self.info, safe_mode=True, enable_attributes=False)) + def torrent(self): + try: + data = b64decode(self.torrent_data) + except TypeError: + return None + data = bdecode(data) + # transform the data into a template-friendly dict + info = data.get('info', {}) + metadata = { + 'comment': data.get('comment', None), + 'created_by': data.get('created by', None), + 'creation_date': None, + 'announce': data.get('announce', None), + 'file_name': info.get('name', None), + 'file_length': info.get('length', None), + 'piece_count': len(info.get('pieces', '')) / 20, + 'piece_length': info.get('piece length', None), + 'url_list': data.get('url-list', []), + 'info_hash': None, + } + if 'creation date' in data: + created= datetime.utcfromtimestamp(data['creation date']) + metadata['creation_date'] = created.replace(tzinfo=utc) + if info: + metadata['info_hash'] = hashlib.sha1(bencode(info)).hexdigest() + + return metadata + for model in (Iso, Test, Release): pre_save.connect(set_created_field, sender=model, diff --git a/requirements.txt b/requirements.txt index 1d5b068e..ae58ee58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ Django==1.4.3 Markdown==2.2.1 South==0.7.6 +bencode==1.0 django-countries==1.5 pgpdump==1.4 -pytz>=2012h +pytz>=2012j diff --git a/requirements_prod.txt b/requirements_prod.txt index f84f46d8..ee1b17ad 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -1,9 +1,10 @@ Django==1.4.3 Markdown==2.2.1 South==0.7.6 +bencode==1.0 django-countries==1.5 pgpdump==1.4 psycopg2==2.4.6 pyinotify==0.9.4 python-memcached==1.48 -pytz>=2012h +pytz>=2012j diff --git a/templates/releng/release_detail.html b/templates/releng/release_detail.html index fec9ce2b..f4de9e52 100644 --- a/templates/releng/release_detail.html +++ b/templates/releng/release_detail.html @@ -9,9 +9,10 @@

    {{ release.version }}

  • Release Date: {{ release.release_date|date }}
  • {% if release.kernel_version %}
  • Kernel Version: {{ release.kernel_version }}
  • {% endif %}
  • Available: {{ release.available|yesno }}
  • - {% if release.available %}
  • Torrent
  • {% endif %} - {% if release.available %}
  • Magnet
  • {% endif %} + {% if release.available %}
  • Download: Torrent, + Magnet
  • {% endif %} + {% if release.torrent_infohash %}
  • Torrent Info Hash: {{ release.torrent_infohash }}
  • {% endif %}
  • Download Size: {% if release.file_size %}{{ release.file_size|filesizeformat }}{% else %}Unknown{% endif %}
  • @@ -20,5 +21,22 @@

    Release Notes

    {{ release.info_html }}
    {% endif %} + + {% if release.torrent_data %}{% with release.torrent as torrent %} +

    Torrent Information

    + +
      +
    • Comment: {{ torrent.comment }}
    • +
    • Creation Date: {{ torrent.creation_date|date:"DATETIME_FORMAT" }} UTC
    • +
    • Created By: {{ torrent.created_by }}
    • +
    • Announce URL: {{ torrent.announce }}
    • +
    • File Name: {{ torrent.file_name }}
    • +
    • File Length: {{ torrent.file_length|filesizeformat }}
    • +
    • Piece Count: {{ torrent.piece_count }} pieces
    • +
    • Piece Length: {{ torrent.piece_length|filesizeformat }}
    • +
    • Computed Info Hash: {{ torrent.info_hash }}
    • +
    • URL List Length: {{ torrent.url_list|length }} URLs
    • +
    + {% endwith %}{% endif %}
    {% endblock %} -- cgit v1.2.3-54-g00ecf From 4d52242f4b5a20a591b5bd44cc0dc12f15a9c92c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 17:19:27 -0600 Subject: Mark release version string as unique It should be unique anyway, but it is especially important now that we are using it in URL patterns for lookup. Signed-off-by: Dan McGee --- .../0006_auto__add_unique_release_version.py | 117 +++++++++++++++++++++ releng/models.py | 2 +- 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 releng/migrations/0006_auto__add_unique_release_version.py diff --git a/releng/migrations/0006_auto__add_unique_release_version.py b/releng/migrations/0006_auto__add_unique_release_version.py new file mode 100644 index 00000000..cb834870 --- /dev/null +++ b/releng/migrations/0006_auto__add_unique_release_version.py @@ -0,0 +1,117 @@ +# -*- coding: 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 unique constraint on 'Release', fields ['version'] + db.create_unique('releng_release', ['version']) + + + def backwards(self, orm): + # Removing unique constraint on 'Release', fields ['version'] + db.delete_unique('releng_release', ['version']) + + + 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'}), + 'removed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}) + }, + '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.release': { + 'Meta': {'ordering': "('-release_date', '-version')", 'object_name': 'Release'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'file_size': ('main.fields.PositiveBigIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'kernel_version': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'release_date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}), + 'torrent_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'torrent_infohash': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'version': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}) + }, + '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.GenericIPAddressField', [], {'max_length': '39'}), + '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'] \ No newline at end of file diff --git a/releng/models.py b/releng/models.py index c7d26966..a0dd57aa 100644 --- a/releng/models.py +++ b/releng/models.py @@ -116,7 +116,7 @@ class Test(models.Model): class Release(models.Model): release_date = models.DateField(db_index=True) - version = models.CharField(max_length=50) + version = models.CharField(max_length=50, unique=True) kernel_version = models.CharField(max_length=50, blank=True) torrent_infohash = models.CharField(max_length=64, blank=True) file_size = PositiveBigIntegerField(null=True, blank=True) -- cgit v1.2.3-54-g00ecf From b642c93aff6bd22013615ae8b51b7a02763e261c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 17:38:54 -0600 Subject: Add a view to download the torrent available for a given release Signed-off-by: Dan McGee --- releng/urls.py | 15 +++++++++++---- releng/views.py | 16 +++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/releng/urls.py b/releng/urls.py index 8413d318..76c36345 100644 --- a/releng/urls.py +++ b/releng/urls.py @@ -11,11 +11,18 @@ (r'^iso/overview/$', 'iso_overview', {}, 'releng-iso-overview'), ) -urlpatterns = patterns('', - (r'^feedback/', include(feedback_patterns)), - (r'^releases/$', +releases_patterns = patterns('releng.views', + (r'^$', ReleaseListView.as_view(), {}, 'releng-release-list'), - (r'^releases/(?P[-.\w]+)/$', + (r'^(?P[-.\w]+)/$', ReleaseDetailView.as_view(), {}, 'releng-release-detail'), + (r'^(?P[-.\w]+)/torrent/$', + 'release_torrent', {}, 'releng-release-torrent'), ) + +urlpatterns = patterns('', + (r'^feedback/', include(feedback_patterns)), + (r'^releases/', include(releases_patterns)), +) + # vim: set ts=4 sw=4 et: diff --git a/releng/views.py b/releng/views.py index 6c49275f..ad4b07d1 100644 --- a/releng/views.py +++ b/releng/views.py @@ -1,7 +1,9 @@ +from base64 import b64decode + from django import forms from django.conf import settings from django.db.models import Count, Max -from django.http import Http404 +from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.views.generic import DetailView, ListView @@ -224,4 +226,16 @@ class ReleaseDetailView(DetailView): slug_field = 'version' slug_url_kwarg = 'version' + +def release_torrent(request, version): + release = get_object_or_404(Release, version=version) + if not release.torrent_data: + raise Http404 + data = b64decode(release.torrent_data) + response = HttpResponse(data, content_type='application/x-bittorrent') + # TODO: this is duplicated from Release.iso_url() + filename = 'archlinux-%s-dual.iso.torrent' % release.version + response['Content-Disposition'] = 'attachment; filename=%s' % filename + return response + # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From dd8e94f69783365160bcbfda61a9bebea5a71324 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 20 Jan 2013 14:53:28 -0600 Subject: Lengthen the mirror rsync IP address field Make it long enough to support a full-form IPv6 address with a subnet. Signed-off-by: Dan McGee --- .../0021_auto__chg_field_mirrorrsync_ip.py | 66 ++++++++++++++++++++++ mirrors/models.py | 5 +- 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 mirrors/migrations/0021_auto__chg_field_mirrorrsync_ip.py diff --git a/mirrors/migrations/0021_auto__chg_field_mirrorrsync_ip.py b/mirrors/migrations/0021_auto__chg_field_mirrorrsync_ip.py new file mode 100644 index 00000000..bbf14bb0 --- /dev/null +++ b/mirrors/migrations/0021_auto__chg_field_mirrorrsync_ip.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.alter_column(u'mirrors_mirrorrsync', 'ip', self.gf('django.db.models.fields.CharField')(max_length=44)) + + def backwards(self, orm): + db.alter_column(u'mirrors_mirrorrsync', 'ip', self.gf('django.db.models.fields.CharField')(max_length=24)) + + models = { + u'mirrors.mirror': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Mirror'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'admin_email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'blank': 'True'}), + 'alternate_email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'blank': 'True'}), + u'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': u"orm['mirrors.Mirror']", 'null': 'True', 'on_delete': 'models.SET_NULL'}) + }, + u'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.TextField', [], {'default': "''", 'blank': 'True'}), + u'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': u"orm['mirrors.MirrorUrl']"}) + }, + u'mirrors.mirrorprotocol': { + 'Meta': {'ordering': "('protocol',)", 'object_name': 'MirrorProtocol'}, + 'default': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + u'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'}) + }, + u'mirrors.mirrorrsync': { + 'Meta': {'object_name': 'MirrorRsync'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.CharField', [], {'max_length': '44'}), + 'mirror': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'rsync_ips'", 'to': u"orm['mirrors.Mirror']"}) + }, + u'mirrors.mirrorurl': { + 'Meta': {'object_name': 'MirrorUrl'}, + 'country': ('django_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', 'blank': 'True'}), + 'has_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'has_ipv6': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mirror': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'urls'", 'to': u"orm['mirrors.Mirror']"}), + 'protocol': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'urls'", 'on_delete': 'models.PROTECT', 'to': u"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 ca421d13..ec4a044d 100644 --- a/mirrors/models.py +++ b/mirrors/models.py @@ -98,11 +98,12 @@ class Meta: class MirrorRsync(models.Model): - ip = models.CharField("IP", max_length=24) + # max length is 40 chars for full-form IPv6 addr + subnet + ip = models.CharField("IP", max_length=44) mirror = models.ForeignKey(Mirror, related_name="rsync_ips") def __unicode__(self): - return "%s" % (self.ip) + return self.ip class Meta: verbose_name = 'mirror rsync IP' -- cgit v1.2.3-54-g00ecf From 1b1b516bd823d807ea81e62fe14fc92c18d0b89d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 20 Jan 2013 14:59:30 -0600 Subject: Query performance enhancements in get_requiredby() For packages with particularly long lists of provides (e.g. perl), the query was getting a bit out of control with the list of names passed in. However, changing it to simply do a subquery resulted in some really poor planning by PostgreSQL. Doing this as a custom 'WHERE' clause utilizing the 'UNION ALL' SQL operator works very well. Signed-off-by: Dan McGee --- main/models.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/main/models.py b/main/models.py index 7155d360..88f0ecd1 100644 --- a/main/models.py +++ b/main/models.py @@ -190,12 +190,13 @@ def get_requiredby(self): category as this package if that check makes sense. """ from packages.models import Depend - provides = self.provides.all() - provide_names = {provide.name for provide in provides} - provide_names.add(self.pkgname) + name_clause = '''packages_depend.name IN ( + SELECT %s UNION ALL + SELECT z.name FROM packages_provision z WHERE z.pkg_id = %s + )''' requiredby = Depend.objects.select_related('pkg', - 'pkg__arch', 'pkg__repo').filter( - name__in=provide_names).order_by( + 'pkg__arch', 'pkg__repo').extra( + where=[name_clause], params=[self.pkgname, self.id]).order_by( 'pkg__pkgname', 'pkg__arch__name', 'pkg__repo__name') if not self.arch.agnostic: # make sure we match architectures if possible -- cgit v1.2.3-54-g00ecf From 45108ea4975419a88c2bb10ed7f3f90d6085d852 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 20 Jan 2013 15:09:18 -0600 Subject: Remove AlwaysCommitMiddleware Let's just go with the Django database option for PostreSQL autocommit mode instead. Signed-off-by: Dan McGee --- main/middleware.py | 40 ---------------------------------------- settings.py | 1 - 2 files changed, 41 deletions(-) delete mode 100644 main/middleware.py diff --git a/main/middleware.py b/main/middleware.py deleted file mode 100644 index a698b13c..00000000 --- a/main/middleware.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.core.exceptions import MiddlewareNotUsed -from django.db import connections - - -class AlwaysCommitMiddleware(object): - """ - Ensure we always commit any possibly open transaction so we leave the - database in a clean state. Without this, pgbouncer et al. always gives - error messages like this for every single request: - - LOG S-0x1accfd0: db/user@unix:5432 new connection to server - LOG C-0x1aaf620: db/user@unix:6432 closing because: client close request (age=0) - LOG S-0x1accfd0: db/user@unix:5432 closing because: unclean server (age=0) - - We only let this middleware apply for PostgreSQL backends; other databases - don't really require connection pooling and thus the reason for this - middleware's use is non-existent. - - The best location of this in your middleware stack is likely the top, as - you want to ensure it happens after any and all database activity has - completed. - """ - def __init__(self): - for conn in connections.all(): - if conn.vendor == 'postgresql': - return - raise MiddlewareNotUsed() - - def process_response(self, request, response): - """Commits any potentially open transactions at the underlying - PostgreSQL database connection level.""" - for conn in connections.all(): - if conn.vendor != 'postgresql': - continue - db_conn = getattr(conn, 'connection', None) - if db_conn is not None: - db_conn.commit() - return response - -# vim: set ts=4 sw=4 et: diff --git a/settings.py b/settings.py index cdc56e3e..8ed5cb61 100644 --- a/settings.py +++ b/settings.py @@ -66,7 +66,6 @@ ) MIDDLEWARE_CLASSES = ( - 'main.middleware.AlwaysCommitMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', -- cgit v1.2.3-54-g00ecf From 53484c45ea82a5afa8bf167f978f657b866d4c93 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 20 Jan 2013 15:27:16 -0600 Subject: Fix error in get_requiredby() when checking provides The query refactor in commit 1b1b516bd removed a queryset I didn't realize was getting used elsewhere in the function. Signed-off-by: Dan McGee --- main/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/main/models.py b/main/models.py index 88f0ecd1..40466d65 100644 --- a/main/models.py +++ b/main/models.py @@ -207,6 +207,7 @@ def get_requiredby(self): # version comparison operators they may specify alpm = AlpmAPI() if alpm.available: + provides = self.provides.all() new_rqd = [] for dep in requiredby: if not dep.comparison or not dep.version: -- cgit v1.2.3-54-g00ecf From a15d0850af0f766cd895b863f62524f1a1b2fe05 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 21 Jan 2013 11:08:35 -0600 Subject: Slight reorganization of urls.py Move some of the not-so-frequently used resources further down in the list of URLs, which will ever so slightly speed up the resolver. Sitemaps don't need to be checked near as often, for instance. Signed-off-by: Dan McGee --- urls.py | 77 ++++++++++++++++++++++++++++++++++++----------------------------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/urls.py b/urls.py index 82882970..97b84a36 100644 --- a/urls.py +++ b/urls.py @@ -21,38 +21,6 @@ admin.autodiscover() urlpatterns = [] -# Feeds patterns, used later -feeds_patterns = patterns('', - (r'^$', 'public.views.feeds', {}, 'feeds-list'), - (r'^news/$', cache_page(300)(NewsFeed())), - (r'^packages/$', cache_page(300)(PackageFeed())), - (r'^packages/(?P[A-z0-9]+)/$', - cache_page(300)(PackageFeed())), - (r'^packages/all/(?P[A-z0-9\-]+)/$', - cache_page(300)(PackageFeed())), - (r'^packages/(?P[A-z0-9]+)/(?P[A-z0-9\-]+)/$', - cache_page(300)(PackageFeed())), - (r'^releases/$', cache_page(300)(ReleaseFeed())), -) - -# Sitemaps -urlpatterns += patterns('', - (r'^sitemap.xml$', - cache_page(1800)(sitemap_views.index), - {'sitemaps': our_sitemaps, 'sitemap_url_name': 'sitemaps'}), - (r'^sitemap-(?P
    .+)\.xml$', - cache_page(1800)(sitemap_views.sitemap), - {'sitemaps': our_sitemaps}, 'sitemaps'), -) - -# Authentication / Admin -urlpatterns += patterns('django.contrib.auth.views', - (r'^login/$', 'login', { - 'template_name': 'registration/login.html'}), - (r'^logout/$', 'logout', { - 'template_name': 'registration/logout.html'}), -) - # Public pages urlpatterns += patterns('public.views', (r'^$', 'index', {}, 'index'), @@ -71,8 +39,18 @@ (r'^master-keys/json/$', 'keys_json', {}, 'pgp-keys-json'), ) -urlpatterns += patterns('retro.views', - (r'^retro/(?P[0-9]{4})/$', 'retro_homepage', {}, 'retro-homepage'), +# Feeds patterns, used below +feeds_patterns = patterns('', + (r'^$', 'public.views.feeds', {}, 'feeds-list'), + (r'^news/$', cache_page(300)(NewsFeed())), + (r'^packages/$', cache_page(300)(PackageFeed())), + (r'^packages/(?P[A-z0-9]+)/$', + cache_page(300)(PackageFeed())), + (r'^packages/all/(?P[A-z0-9\-]+)/$', + cache_page(300)(PackageFeed())), + (r'^packages/(?P[A-z0-9]+)/(?P[A-z0-9\-]+)/$', + cache_page(300)(PackageFeed())), + (r'^releases/$', cache_page(300)(ReleaseFeed())), ) # Includes and other remaining stuff @@ -97,6 +75,30 @@ (r'^todolists/$','todolists.views.public_list'), ) +# Retro home page views +urlpatterns += patterns('retro.views', + (r'^retro/(?P[0-9]{4})/$', 'retro_homepage', {}, 'retro-homepage'), +) + +# Sitemaps +urlpatterns += patterns('', + (r'^sitemap.xml$', + cache_page(1800)(sitemap_views.index), + {'sitemaps': our_sitemaps, 'sitemap_url_name': 'sitemaps'}), + (r'^sitemap-(?P
    .+)\.xml$', + cache_page(1800)(sitemap_views.sitemap), + {'sitemaps': our_sitemaps}, 'sitemaps'), +) + +# Authentication / Admin +urlpatterns += patterns('django.contrib.auth.views', + (r'^login/$', 'login', { + 'template_name': 'registration/login.html'}), + (r'^logout/$', 'logout', { + 'template_name': 'registration/logout.html'}), +) + +# Redirects for older known pages we see in the logs legacy_urls = ( ('^about.php', '/about/'), ('^changelog.php', '/packages/?sort=-last_update'), @@ -120,4 +122,11 @@ urlpatterns += [url(old_url, RedirectView.as_view(url=new_url)) for old_url, new_url in legacy_urls] + +def show_urls(urllist=urlpatterns, depth=0): + for entry in urllist: + print " " * depth, entry.regex.pattern + if hasattr(entry, 'url_patterns'): + show_urls(entry.url_patterns, depth + 1) + # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From f9252df1138ae388168cf76cb3d654a2abbce4ec Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 21 Jan 2013 15:14:44 -0600 Subject: Switch to using the cached STATICFILES_STORAGE backend This should finally let us crank up the Expires: header to far-future values in production since updates to JS and CSS files will take effect immediately. Some minor removals were made from retro stylesheets as they were referencing files that don't actually exist because they were missing from the web archive. Signed-off-by: Dan McGee --- retro/static/2002/main.css | 1 - retro/static/2003/main.css | 5 ++--- settings.py | 3 +++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/retro/static/2002/main.css b/retro/static/2002/main.css index fb8d4a68..ea33cfae 100644 --- a/retro/static/2002/main.css +++ b/retro/static/2002/main.css @@ -89,7 +89,6 @@ ul.list { } table.border { - background-image: url('bg.gif'); background-repeat: no-repeat; background-color: #000000; border-bottom: #cccccc 1px solid; diff --git a/retro/static/2003/main.css b/retro/static/2003/main.css index a9aa1dd8..b9b2330d 100644 --- a/retro/static/2003/main.css +++ b/retro/static/2003/main.css @@ -140,7 +140,7 @@ table.box { } table.header { - background: #000000 url('bg.gif') no-repeat; + background: #000000; border-bottom: #cccccc 1px solid; border-left: #cccccc 1px solid; border-right: #cccccc 1px solid; @@ -231,7 +231,6 @@ th { } th.row { - background: url('grid.png'); } th.rowhdr { @@ -243,7 +242,7 @@ th.rowhdr { td.box_headline { color: #dddddd; - background: #000000 url('bg.gif') no-repeat; + background: #000000; border-bottom: #cccccc 1px solid; padding: 0px; } diff --git a/settings.py b/settings.py index 8ed5cb61..dbc06159 100644 --- a/settings.py +++ b/settings.py @@ -89,6 +89,9 @@ os.path.join(DEPLOY_PATH, 'sitestatic'), ) +# Static files backend that allows us to use far future Expires headers +STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.CachedStaticFilesStorage' + # Configure where messages should reside MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' -- cgit v1.2.3-54-g00ecf From e9e1c071654edd7b95e20c8105abbc23f426cecc Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 22 Jan 2013 16:47:43 -0600 Subject: Show staging version on todolist view page If one exists, it is easy enough to show it here so in-progress todolists can easily be cross-checked with the current state of the repository. Signed-off-by: Dan McGee --- templates/todolists/view.html | 5 +++++ todolists/utils.py | 19 +++++++++++++++++++ todolists/views.py | 3 ++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/templates/todolists/view.html b/templates/todolists/view.html index 86221127..e544fa12 100644 --- a/templates/todolists/view.html +++ b/templates/todolists/view.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load static from staticfiles %} +{% load package_extras %} {% load todolists %} {% block title %}Arch Linux - Todo: {{ list.name }}{% endblock %} @@ -62,6 +63,7 @@

    Filter Todo List Packages

    Repository Name Current Version + Staging Version Maintainers Status @@ -77,6 +79,9 @@

    Filter Todo List Packages

    {% else %} {{ pkg.pkg.full_version }} {% endif %} + {% with staging=pkg.staging %} + {% pkg_details_link staging staging.full_version %} + {% endwith %} {{ pkg.maintainers|join:', ' }} {% if perms.todolists.change_todolistpackage %} diff --git a/todolists/utils.py b/todolists/utils.py index e86d9054..51a75a3c 100644 --- a/todolists/utils.py +++ b/todolists/utils.py @@ -2,6 +2,7 @@ from django.db.models import Count from .models import Todolist, TodolistPackage +from packages.models import Package def todo_counts(): @@ -36,4 +37,22 @@ def get_annotated_todolists(incomplete_only=False): return lists + +def attach_staging(packages, list_id): + '''Look for any staging version of the packages provided and attach them + to the 'staging' attribute on each package if found.''' + pkgnames = TodolistPackage.objects.filter( + todolist_id=list_id).values('pkgname') + staging_pkgs = Package.objects.normal().filter(repo__staging=True, + pkgname__in=pkgnames) + # now build a lookup dict to attach to the correct package + lookup = {(p.pkgname, p.arch): p for p in staging_pkgs} + + annotated = [] + for package in packages: + in_staging = lookup.get((package.pkgname, package.arch), None) + package.staging = in_staging + + return annotated + # vim: set ts=4 sw=4 et: diff --git a/todolists/views.py b/todolists/views.py index fcf62e23..f333728a 100644 --- a/todolists/views.py +++ b/todolists/views.py @@ -16,7 +16,7 @@ from main.utils import find_unique_slug from packages.utils import attach_maintainers from .models import Todolist, TodolistPackage -from .utils import get_annotated_todolists +from .utils import get_annotated_todolists, attach_staging class TodoListForm(forms.ModelForm): @@ -69,6 +69,7 @@ def view(request, slug): # we don't hold onto the result, but the objects are the same here, # so accessing maintainers in the template is now cheap attach_maintainers(todolist.packages()) + attach_staging(todolist.packages(), todolist.pk) arches = {tp.arch for tp in todolist.packages()} repos = {tp.repo for tp in todolist.packages()} return render(request, 'todolists/view.html', { -- cgit v1.2.3-54-g00ecf From 2c958511c41f53fb7de49ed4662eec966e0b76a5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 22 Jan 2013 16:48:49 -0600 Subject: Use a subquery rather than two queries in attach_maintainers Now that we are using a database that doesn't stink, it makes more sense to do all of the stuff we need to do down at the database level. This helps a lot when 500+ packages are in play at a given time, such as some of our larger rebuild todo lists. Signed-off-by: Dan McGee --- packages/utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/utils.py b/packages/utils.py index a72404f4..49aeb8ce 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -6,6 +6,7 @@ from django.core.serializers.json import DjangoJSONEncoder from django.db import connection from django.db.models import Count, Max, F +from django.db.models.query import QuerySet from django.contrib.auth.models import User from main.models import Package, PackageFile, Arch, Repo @@ -253,8 +254,11 @@ def attach_maintainers(packages): '''Given a queryset or something resembling it of package objects, find all the maintainers and attach them to the packages to prevent N+1 query cascading.''' - packages = list(packages) - pkgbases = {p.pkgbase for p in packages if p is not None} + if isinstance(packages, QuerySet): + pkgbases = packages.values('pkgbase') + else: + packages = list(packages) + pkgbases = {p.pkgbase for p in packages if p is not None} rels = PackageRelation.objects.filter(type=PackageRelation.MAINTAINER, pkgbase__in=pkgbases).values_list( 'pkgbase', 'user_id').order_by().distinct() -- cgit v1.2.3-54-g00ecf From 85dc9d6df9f9038be6049aefbf8eb9ea94f9f23c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 22 Jan 2013 17:20:41 -0700 Subject: Override the default admin queryset for some models Because some attributes are optional or otherwise not auto-magically picked up by Django, we can help the performance of loading these pages a lot by forcing a select_related() on the queryset used by the admin. For something like signoff_specifications, this drops the query count from ~107 to 9 queries. Signed-off-by: Dan McGee --- packages/admin.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/admin.py b/packages/admin.py index 5e32dbb4..820bbb29 100644 --- a/packages/admin.py +++ b/packages/admin.py @@ -3,6 +3,7 @@ from .models import (PackageRelation, FlagRequest, Signoff, SignoffSpecification, Update) + class PackageRelationAdmin(admin.ModelAdmin): list_display = ('pkgbase', 'user', 'type', 'created') list_filter = ('type', 'user') @@ -19,6 +20,10 @@ class FlagRequestAdmin(admin.ModelAdmin): ordering = ('-created',) date_hierarchy = 'created' + def queryset(self, request): + qs = super(FlagRequestAdmin, self).queryset(request) + return qs.select_related('repo', 'user') + class SignoffAdmin(admin.ModelAdmin): list_display = ('pkgbase', 'full_version', 'arch', 'repo', @@ -28,6 +33,7 @@ class SignoffAdmin(admin.ModelAdmin): ordering = ('-created',) date_hierarchy = 'created' + class SignoffSpecificationAdmin(admin.ModelAdmin): list_display = ('pkgbase', 'full_version', 'arch', 'repo', 'user', 'created', 'comments') @@ -36,6 +42,10 @@ class SignoffSpecificationAdmin(admin.ModelAdmin): ordering = ('-created',) date_hierarchy = 'created' + def queryset(self, request): + qs = super(SignoffSpecificationAdmin, self).queryset(request) + return qs.select_related('arch', 'repo', 'user') + class UpdateAdmin(admin.ModelAdmin): list_display = ('pkgname', 'repo', 'arch', 'action_flag', -- cgit v1.2.3-54-g00ecf From 69fd83df03806585c3b7d6b115af916a73f4ae41 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 22 Jan 2013 20:21:40 -0700 Subject: Spice up the release listing page a bit Add JS tablesorter code and add some style to the yesno column. Signed-off-by: Dan McGee --- sitestatic/archweb.css | 8 ++++++++ templates/releng/release_list.html | 19 +++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index f43bba1f..dcc964ee 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -1079,6 +1079,14 @@ ul.signoff-list { color: red; } +#release-list .available-yes { + color: green; +} + +#release-list .available-no { + color: red; +} + #key-status .signed-yes { color: green; } diff --git a/templates/releng/release_list.html b/templates/releng/release_list.html index 1657249f..84008541 100644 --- a/templates/releng/release_list.html +++ b/templates/releng/release_list.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load url from future %} +{% load static from staticfiles %} {% block title %}Arch Linux - Releases{% endblock %} @@ -9,7 +10,6 @@ {% block content %}
    -

    Releases

    @@ -28,9 +28,9 @@

    Releases

    {% for item in release_list %} - + - + @@ -39,6 +39,17 @@

    Releases

    {% endfor %}
    {{ item.release_date|date }}{{ item.version }}{{ item.version }} {{ item.kernel_version|default:"" }}{{ item.available|yesno }}{{ item.available|yesno|capfirst }} {% if item.available %}Torrent{% endif %} {% if item.available %}Magnet{% endif %}
    -
    + +{% load cdn %}{% jquery %}{% jquery_tablesorter %} + + {% endblock %} -- cgit v1.2.3-54-g00ecf From be49f26a815cca589c625ff8dd99c85a80262281 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 22 Jan 2013 20:58:50 -0700 Subject: Slight optimization when searching for removed package Signed-off-by: Dan McGee --- packages/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/models.py b/packages/models.py index ef86d8e9..ff677883 100644 --- a/packages/models.py +++ b/packages/models.py @@ -318,7 +318,8 @@ def new_version(self): return u'%s-%s' % (self.new_pkgver, self.new_pkgrel) def elsewhere(self): - return Package.objects.filter(pkgname=self.pkgname, arch=self.arch) + return Package.objects.normal().filter( + pkgname=self.pkgname, arch=self.arch) def __unicode__(self): return u'%s of %s on %s' % (self.get_action_flag_display(), -- cgit v1.2.3-54-g00ecf From a10798b756bbfc5d8dbad76546ca670efca75e56 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 23 Jan 2013 09:18:59 -0700 Subject: Use querysets for calls to get_object_or_404(Package) This works better in most cases since we need the architecture and repository objects at some point during the view process. Signed-off-by: Dan McGee --- packages/views/display.py | 8 ++++---- packages/views/flag.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/views/display.py b/packages/views/display.py index 445c1abe..c2369aba 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -168,7 +168,7 @@ def group_details(request, arch, name): def files(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) # files are inserted in sorted order, so preserve that fileslist = PackageFile.objects.filter(pkg=pkg).order_by('id') @@ -185,14 +185,14 @@ def files(request, name, repo, arch): def details_json(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) to_json = json.dumps(pkg, ensure_ascii=False, cls=PackageJSONEncoder) return HttpResponse(to_json, content_type='application/json') def files_json(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) # files are inserted in sorted order, so preserve that fileslist = PackageFile.objects.filter(pkg=pkg).order_by('id') @@ -213,7 +213,7 @@ def files_json(request, name, repo, arch): def download(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) url = get_mirror_url_for_download() if not url: diff --git a/packages/views/flag.py b/packages/views/flag.py index dadadd19..edb3f092 100644 --- a/packages/views/flag.py +++ b/packages/views/flag.py @@ -49,7 +49,7 @@ def flaghelp(request): @never_cache def flag(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) if pkg.flag_date is not None: # already flagged. do nothing. @@ -158,7 +158,7 @@ def flag_confirmed(request, name, repo, arch): @permission_required('main.change_package') def unflag(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) pkg.flag_date = None pkg.save() @@ -166,7 +166,7 @@ def unflag(request, name, repo, arch): @permission_required('main.change_package') def unflag_all(request, name, repo, arch): - pkg = get_object_or_404(Package, + pkg = get_object_or_404(Package.objects.normal(), pkgname=name, repo__name__iexact=repo, arch__name=arch) # find all packages from (hopefully) the same PKGBUILD pkgs = Package.objects.filter(pkgbase=pkg.pkgbase, -- cgit v1.2.3-54-g00ecf From dc6cc49f6f876983f76f5f8c05a2285801f27ea0 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 23 Jan 2013 09:20:19 -0700 Subject: Use more modern verison of string template formatting Signed-off-by: Dan McGee --- packages/views/display.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/views/display.py b/packages/views/display.py index c2369aba..497c8d48 100644 --- a/packages/views/display.py +++ b/packages/views/display.py @@ -1,6 +1,5 @@ import datetime import json -from string import Template from urllib import urlencode from django.http import HttpResponse, Http404 @@ -223,12 +222,9 @@ def download(request, name, repo, arch): # grab the first non-any arch to fake the download path arch = Arch.objects.exclude(agnostic=True)[0].name values = { - 'host': url.url, - 'arch': arch, - 'repo': pkg.repo.name.lower(), - 'file': pkg.filename, } - url = Template('${host}${repo}/os/${arch}/${file}').substitute(values) + url = '{host}{repo}/os/{arch}/{filename}'.format(host=url.url, + repo=pkg.repo.name.lower(), arch=arch, filename=pkg.filename) return redirect(url) # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 44dde6f02f649244d7c1b9dcf03d8ce592bbbbcb Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 27 Jan 2013 09:44:38 -0600 Subject: Fix todolist maintainer sorting And also fix up a place where we dereferenced a variable in a template that doesn't exist. Signed-off-by: Dan McGee --- templates/todolists/view.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/todolists/view.html b/templates/todolists/view.html index e544fa12..934e3ae8 100644 --- a/templates/todolists/view.html +++ b/templates/todolists/view.html @@ -80,7 +80,7 @@

    Filter Todo List Packages

    {{ pkg.pkg.full_version }} {% endif %} {% with staging=pkg.staging %} - {% pkg_details_link staging staging.full_version %} + {% if staging %}{% pkg_details_link staging staging.full_version %}{% endif %} {% endwith %} {{ pkg.maintainers|join:', ' }} @@ -103,7 +103,7 @@

    Filter Todo List Packages

    $(".results").tablesorter({ widgets: ['zebra'], sortList: [[2,0], [0,0]], - headers: { 5: { sorter: 'todostatus' } } + headers: { 6: { sorter: 'todostatus' } } }); }); $(document).ready(function() { -- cgit v1.2.3-54-g00ecf From 1a55c0f0bc8359f85f6c23d69cdf3ca05a4d25a2 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 28 Jan 2013 13:50:33 -0700 Subject: Don't error on empty torrent data Signed-off-by: Dan McGee --- releng/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/releng/models.py b/releng/models.py index a0dd57aa..19040905 100644 --- a/releng/models.py +++ b/releng/models.py @@ -160,6 +160,8 @@ def torrent(self): data = b64decode(self.torrent_data) except TypeError: return None + if not data: + return None data = bdecode(data) # transform the data into a template-friendly dict info = data.get('info', {}) -- cgit v1.2.3-54-g00ecf From 7d4a8b9adf353d7adce4c3c22101e774092eb4de Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 28 Jan 2013 14:06:41 -0700 Subject: Add MD5 and SHA1 fields for releases Signed-off-by: Dan McGee --- ...dd_field_release_md5__add_field_release_sha1.py | 122 +++++++++++++++++++++ releng/models.py | 4 +- templates/releng/release_detail.html | 2 + 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 releng/migrations/0007_auto__add_field_release_md5__add_field_release_sha1.py diff --git a/releng/migrations/0007_auto__add_field_release_md5__add_field_release_sha1.py b/releng/migrations/0007_auto__add_field_release_md5__add_field_release_sha1.py new file mode 100644 index 00000000..f76be3d7 --- /dev/null +++ b/releng/migrations/0007_auto__add_field_release_md5__add_field_release_sha1.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +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('releng_release', 'md5_sum', + self.gf('django.db.models.fields.CharField')(default='', max_length=32, blank=True), + keep_default=False) + db.add_column('releng_release', 'sha1_sum', + self.gf('django.db.models.fields.CharField')(default='', max_length=40, blank=True), + keep_default=False) + db.alter_column('releng_release', 'torrent_infohash', self.gf('django.db.models.fields.CharField')(max_length=40)) + + def backwards(self, orm): + db.delete_column('releng_release', 'md5_sum') + db.delete_column('releng_release', 'sha1_sum') + db.alter_column('releng_release', 'torrent_infohash', self.gf('django.db.models.fields.CharField')(max_length=64)) + + 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'}), + 'removed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}) + }, + '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.release': { + 'Meta': {'ordering': "('-release_date', '-version')", 'object_name': 'Release'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'file_size': ('main.fields.PositiveBigIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'kernel_version': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'md5_sum': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'release_date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}), + 'sha1_sum': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'torrent_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'torrent_infohash': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'version': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}) + }, + '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.GenericIPAddressField', [], {'max_length': '39'}), + '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/models.py b/releng/models.py index 19040905..dd034e7f 100644 --- a/releng/models.py +++ b/releng/models.py @@ -118,7 +118,9 @@ class Release(models.Model): release_date = models.DateField(db_index=True) version = models.CharField(max_length=50, unique=True) kernel_version = models.CharField(max_length=50, blank=True) - torrent_infohash = models.CharField(max_length=64, blank=True) + torrent_infohash = models.CharField(max_length=40, blank=True) + md5_sum = models.CharField('MD5 digest', max_length=32, blank=True) + sha1_sum = models.CharField('SHA1 digest', max_length=40, blank=True) file_size = PositiveBigIntegerField(null=True, blank=True) created = models.DateTimeField(editable=False) available = models.BooleanField(default=True) diff --git a/templates/releng/release_detail.html b/templates/releng/release_detail.html index f4de9e52..ea54be66 100644 --- a/templates/releng/release_detail.html +++ b/templates/releng/release_detail.html @@ -13,6 +13,8 @@

    {{ release.version }}

    title="Download torrent for {{ release.version }}">Torrent, Magnet{% endif %} {% if release.torrent_infohash %}
  • Torrent Info Hash: {{ release.torrent_infohash }}
  • {% endif %} + {% if release.md5_sum %}
  • MD5: {{ release.md5_sum }}
  • {% endif %} + {% if release.sha1_sum %}
  • SHA1: {{ release.sha1_sum }}
  • {% endif %}
  • Download Size: {% if release.file_size %}{{ release.file_size|filesizeformat }}{% else %}Unknown{% endif %}
  • -- cgit v1.2.3-54-g00ecf From bc539b6ed174fed1545aabaa4ceb7a7f925cbbed Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 28 Jan 2013 14:13:53 -0700 Subject: Extract torrent trackers into a settings variable This allows them to be overridden and changed in a central location, like we do with the SVN URL, PXE boot URL, etc. Signed-off-by: Dan McGee --- releng/models.py | 5 +++-- settings.py | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/releng/models.py b/releng/models.py index dd034e7f..b95f7d52 100644 --- a/releng/models.py +++ b/releng/models.py @@ -5,6 +5,7 @@ import markdown from pytz import utc +from django.conf import settings from django.core.urlresolvers import reverse from django.db import models from django.db.models.signals import pre_save @@ -146,9 +147,9 @@ def iso_url(self): def magnet_uri(self): query = [ ('dn', "archlinux-%s-dual.iso" % self.version), - ('tr', "udp://tracker.archlinux.org:6969"), - ('tr', "http://tracker.archlinux.org:6969/announce"), ] + if settings.TORRENT_TRACKERS: + query.extend(('tr', uri) for uri in settings.TORRENT_TRACKERS) if self.torrent_infohash: query.insert(0, ('xt', "urn:btih:%s" % self.torrent_infohash)) return "magnet:?%s" % '&'.join(['%s=%s' % (k, v) for k, v in query]) diff --git a/settings.py b/settings.py index dbc06159..c856bf57 100644 --- a/settings.py +++ b/settings.py @@ -164,6 +164,12 @@ # community bit on the end, repo.svn_root is appended) SVN_BASE_URL = 'svn://svn.archlinux.org/' +# Trackers used for ISO download magnet links +TORRENT_TRACKERS = ( + 'udp://tracker.archlinux.org:6969', + 'http://tracker.archlinux.org:6969/announce', +) + ## Import local settings from local_settings import * -- cgit v1.2.3-54-g00ecf From a471316a58a9c62b869696fe36d72abcbf9f2ab1 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 28 Jan 2013 14:27:44 -0700 Subject: Use torrent view and checksums where appropriate We no longer need to link externally to these items since we have all the data available in the web application now. Signed-off-by: Dan McGee --- templates/public/download.html | 12 +++++------- templates/releng/release_detail.html | 5 ++++- templates/releng/release_list.html | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/templates/public/download.html b/templates/public/download.html index 29490849..f385ea29 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -56,8 +56,8 @@

    BitTorrent Download (recommended)

    If you can spare the bytes, please leave the client open after your download is finished, so you can seed it back to others. A web-seed capable client is recommended for fastest download speeds.

    -

    Download torrent for {{ release.version }} +

    Download torrent for {{ release.version }} (Magnet) {% if release.file_size %}({{ release.file_size|filesizeformat }}){% endif %}

    @@ -81,11 +81,9 @@

    Checksums

    • PGP signature
    • -
    • SHA1 checksums
    • -
    • MD5 checksums
    • + title="PGP signature">PGP signature + {% if release.md5_sum %}
    • MD5: {{ release.md5_sum }}
    • {% endif %} + {% if release.sha1_sum %}
    • SHA1: {{ release.sha1_sum }}
    • {% endif %}
    {% cache 600 download-mirrors %} diff --git a/templates/releng/release_detail.html b/templates/releng/release_detail.html index ea54be66..01c0319e 100644 --- a/templates/releng/release_detail.html +++ b/templates/releng/release_detail.html @@ -1,4 +1,6 @@ {% extends "base.html" %} +{% load url from future %} + {% block title %}Arch Linux - Release: {{ release.version }}{% endblock %} {% block content %} @@ -9,7 +11,8 @@

    {{ release.version }}

  • Release Date: {{ release.release_date|date }}
  • {% if release.kernel_version %}
  • Kernel Version: {{ release.kernel_version }}
  • {% endif %}
  • Available: {{ release.available|yesno }}
  • - {% if release.available %}
  • Download: Download: Torrent, Magnet
  • {% endif %} {% if release.torrent_infohash %}
  • Torrent Info Hash: {{ release.torrent_infohash }}
  • {% endif %} diff --git a/templates/releng/release_list.html b/templates/releng/release_list.html index 84008541..ef53a93d 100644 --- a/templates/releng/release_list.html +++ b/templates/releng/release_list.html @@ -31,7 +31,7 @@

    Releases

    {{ item.version }} {{ item.kernel_version|default:"" }} {{ item.available|yesno|capfirst }} - {% if item.available %}{% if item.available %}Torrent{% endif %} {% if item.available %}Magnet{% endif %} {% if item.file_size %}{{ item.file_size|filesizeformat }}{% endif %} -- cgit v1.2.3-54-g00ecf From 6cf98552f1c83d8e4fca1526131febd17045eae7 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 28 Jan 2013 14:48:37 -0700 Subject: Slight reorginization in the download page template Move things up that don't belong to the torrent section; make magnet link more prominent by using a bulleted list. Signed-off-by: Dan McGee --- templates/public/download.html | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/templates/public/download.html b/templates/public/download.html index f385ea29..c68cf66b 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -28,6 +28,7 @@

    Release Info

    • Current Release: {{ release.version }}
    • {% if release.kernel_version %}
    • Included Kernel: {{ release.kernel_version }}
    • {% endif %} + {% if release.file_size %}
    • ISO Size: {{ release.file_size|filesizeformat }}
    • {% endif %}
    • Installation Guide
    • Resources:
        @@ -56,17 +57,21 @@

        BitTorrent Download (recommended)

        If you can spare the bytes, please leave the client open after your download is finished, so you can seed it back to others. A web-seed capable client is recommended for fastest download speeds.

        -

        Download torrent for {{ release.version }} - (Magnet) - {% if release.file_size %}({{ release.file_size|filesizeformat }}){% endif %} -

        + +

        Netboot

        If you have a wired connection, you can boot the latest release directly over the network.

        -

        Arch Linux Netboot

        +

        HTTP Direct Downloads

        -- cgit v1.2.3-54-g00ecf From 839371f3f8a7b6de199c48735098567db297928a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 28 Jan 2013 23:00:18 -0600 Subject: Fix missing template variable for removed todolist packages When there was no longer an attached package, running in template debug mode showed we were trying to dereference a variable that didn't exist. Fix the issue by adding a bit more to the if conditional block. Signed-off-by: Dan McGee --- templates/todolists/view.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/todolists/view.html b/templates/todolists/view.html index 934e3ae8..1b9a9e37 100644 --- a/templates/todolists/view.html +++ b/templates/todolists/view.html @@ -76,8 +76,10 @@

        Filter Todo List Packages

        {% todopkg_details_link pkg %} {% if pkg.pkg.flag_date %} {{ pkg.pkg.full_version }} - {% else %} + {% elif pkg.pkg %} {{ pkg.pkg.full_version }} + {% else %} + {% endif %} {% with staging=pkg.staging %} {% if staging %}{% pkg_details_link staging staging.full_version %}{% endif %} -- cgit v1.2.3-54-g00ecf From e969da2d40ed3256a89ff10d627a11e70a451b6a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 31 Jan 2013 08:49:50 -0600 Subject: Run pngcrush and optipng over most static content The programs have improved a bit and found some room for optimization, especially in the static logo content. Some files were reduced in size by 50% or more. Signed-off-by: Dan McGee --- .../static/logos/archlinux-logo-black-1200dpi.png | Bin 283011 -> 131811 bytes public/static/logos/archlinux-logo-black-90dpi.png | Bin 12971 -> 8291 bytes .../static/logos/archlinux-logo-dark-1200dpi.png | Bin 291912 -> 260724 bytes public/static/logos/archlinux-logo-dark-90dpi.png | Bin 13805 -> 11178 bytes .../static/logos/archlinux-logo-light-1200dpi.png | Bin 284099 -> 251334 bytes public/static/logos/archlinux-logo-light-90dpi.png | Bin 13084 -> 11047 bytes .../static/logos/archlinux-logo-white-1200dpi.png | Bin 263771 -> 131811 bytes public/static/logos/archlinux-logo-white-90dpi.png | Bin 11870 -> 8291 bytes .../static/logos/legacy/arch-legacy-aqua-blue.png | Bin 11150 -> 10281 bytes .../static/logos/legacy/arch-legacy-aqua-white.png | Bin 9171 -> 5527 bytes public/static/logos/legacy/arch-legacy-aqua.png | Bin 7709 -> 7177 bytes public/static/logos/legacy/arch-legacy-blue1.png | Bin 6563 -> 5108 bytes public/static/logos/legacy/arch-legacy-blue2.png | Bin 4588 -> 3510 bytes .../logos/legacy/arch-legacy-noodle-blue.png | Bin 13223 -> 12231 bytes .../static/logos/legacy/arch-legacy-noodle-box.png | Bin 12060 -> 11235 bytes .../static/logos/legacy/arch-legacy-noodle-cup.png | Bin 9971 -> 9446 bytes .../logos/legacy/arch-legacy-noodle-white.png | Bin 11340 -> 9589 bytes public/static/logos/legacy/arch-legacy-ribbon1.png | Bin 11628 -> 5255 bytes public/static/logos/legacy/arch-legacy-ribbon2.png | Bin 12390 -> 5361 bytes public/static/logos/legacy/arch-legacy-ribbon3.png | Bin 15590 -> 6467 bytes public/static/logos/legacy/arch-legacy-ribbon4.png | Bin 16747 -> 6813 bytes public/static/logos/legacy/arch-legacy-ribbon5.png | Bin 4986 -> 4896 bytes public/static/logos/legacy/arch-legacy-ribbon6.png | Bin 15700 -> 6457 bytes .../static/logos/legacy/arch-legacy-wombat-lg.png | Bin 114926 -> 99457 bytes public/static/logos/legacy/arch-legacy-wombat.png | Bin 7761 -> 7102 bytes retro/static/2007/logo.png | Bin 15730 -> 14892 bytes retro/static/2008/logo.png | Bin 15730 -> 14892 bytes retro/static/2009/sevenl_button.png | Bin 9028 -> 6840 bytes 28 files changed, 0 insertions(+), 0 deletions(-) diff --git a/public/static/logos/archlinux-logo-black-1200dpi.png b/public/static/logos/archlinux-logo-black-1200dpi.png index a3082c39..3b6b6e48 100644 Binary files a/public/static/logos/archlinux-logo-black-1200dpi.png and b/public/static/logos/archlinux-logo-black-1200dpi.png differ diff --git a/public/static/logos/archlinux-logo-black-90dpi.png b/public/static/logos/archlinux-logo-black-90dpi.png index 6948b795..528f9d62 100644 Binary files a/public/static/logos/archlinux-logo-black-90dpi.png and b/public/static/logos/archlinux-logo-black-90dpi.png differ diff --git a/public/static/logos/archlinux-logo-dark-1200dpi.png b/public/static/logos/archlinux-logo-dark-1200dpi.png index 24a5cefa..62eeba12 100644 Binary files a/public/static/logos/archlinux-logo-dark-1200dpi.png and b/public/static/logos/archlinux-logo-dark-1200dpi.png differ diff --git a/public/static/logos/archlinux-logo-dark-90dpi.png b/public/static/logos/archlinux-logo-dark-90dpi.png index f3757c61..8830fe11 100644 Binary files a/public/static/logos/archlinux-logo-dark-90dpi.png and b/public/static/logos/archlinux-logo-dark-90dpi.png differ diff --git a/public/static/logos/archlinux-logo-light-1200dpi.png b/public/static/logos/archlinux-logo-light-1200dpi.png index 79e0a0f1..d5399e9a 100644 Binary files a/public/static/logos/archlinux-logo-light-1200dpi.png and b/public/static/logos/archlinux-logo-light-1200dpi.png differ diff --git a/public/static/logos/archlinux-logo-light-90dpi.png b/public/static/logos/archlinux-logo-light-90dpi.png index 95803309..e5f4055d 100644 Binary files a/public/static/logos/archlinux-logo-light-90dpi.png and b/public/static/logos/archlinux-logo-light-90dpi.png differ diff --git a/public/static/logos/archlinux-logo-white-1200dpi.png b/public/static/logos/archlinux-logo-white-1200dpi.png index 50e700cf..4c287e32 100644 Binary files a/public/static/logos/archlinux-logo-white-1200dpi.png and b/public/static/logos/archlinux-logo-white-1200dpi.png differ diff --git a/public/static/logos/archlinux-logo-white-90dpi.png b/public/static/logos/archlinux-logo-white-90dpi.png index 86679601..3c7173bc 100644 Binary files a/public/static/logos/archlinux-logo-white-90dpi.png and b/public/static/logos/archlinux-logo-white-90dpi.png differ diff --git a/public/static/logos/legacy/arch-legacy-aqua-blue.png b/public/static/logos/legacy/arch-legacy-aqua-blue.png index 9637ce72..4bbb215d 100644 Binary files a/public/static/logos/legacy/arch-legacy-aqua-blue.png and b/public/static/logos/legacy/arch-legacy-aqua-blue.png differ diff --git a/public/static/logos/legacy/arch-legacy-aqua-white.png b/public/static/logos/legacy/arch-legacy-aqua-white.png index 25fe9001..68ae73b6 100644 Binary files a/public/static/logos/legacy/arch-legacy-aqua-white.png and b/public/static/logos/legacy/arch-legacy-aqua-white.png differ diff --git a/public/static/logos/legacy/arch-legacy-aqua.png b/public/static/logos/legacy/arch-legacy-aqua.png index 881e1709..8cc7da47 100644 Binary files a/public/static/logos/legacy/arch-legacy-aqua.png and b/public/static/logos/legacy/arch-legacy-aqua.png differ diff --git a/public/static/logos/legacy/arch-legacy-blue1.png b/public/static/logos/legacy/arch-legacy-blue1.png index 3ed6c248..403a0661 100644 Binary files a/public/static/logos/legacy/arch-legacy-blue1.png and b/public/static/logos/legacy/arch-legacy-blue1.png differ diff --git a/public/static/logos/legacy/arch-legacy-blue2.png b/public/static/logos/legacy/arch-legacy-blue2.png index 8b5b791e..809ad4f0 100644 Binary files a/public/static/logos/legacy/arch-legacy-blue2.png and b/public/static/logos/legacy/arch-legacy-blue2.png differ diff --git a/public/static/logos/legacy/arch-legacy-noodle-blue.png b/public/static/logos/legacy/arch-legacy-noodle-blue.png index b24d34cf..cf7c00ed 100644 Binary files a/public/static/logos/legacy/arch-legacy-noodle-blue.png and b/public/static/logos/legacy/arch-legacy-noodle-blue.png differ diff --git a/public/static/logos/legacy/arch-legacy-noodle-box.png b/public/static/logos/legacy/arch-legacy-noodle-box.png index 1162ed64..78d76a6a 100644 Binary files a/public/static/logos/legacy/arch-legacy-noodle-box.png and b/public/static/logos/legacy/arch-legacy-noodle-box.png differ diff --git a/public/static/logos/legacy/arch-legacy-noodle-cup.png b/public/static/logos/legacy/arch-legacy-noodle-cup.png index b4f93078..c62cceee 100644 Binary files a/public/static/logos/legacy/arch-legacy-noodle-cup.png and b/public/static/logos/legacy/arch-legacy-noodle-cup.png differ diff --git a/public/static/logos/legacy/arch-legacy-noodle-white.png b/public/static/logos/legacy/arch-legacy-noodle-white.png index a12ee21c..04c17c53 100644 Binary files a/public/static/logos/legacy/arch-legacy-noodle-white.png and b/public/static/logos/legacy/arch-legacy-noodle-white.png differ diff --git a/public/static/logos/legacy/arch-legacy-ribbon1.png b/public/static/logos/legacy/arch-legacy-ribbon1.png index fb8e7720..dba79302 100644 Binary files a/public/static/logos/legacy/arch-legacy-ribbon1.png and b/public/static/logos/legacy/arch-legacy-ribbon1.png differ diff --git a/public/static/logos/legacy/arch-legacy-ribbon2.png b/public/static/logos/legacy/arch-legacy-ribbon2.png index 66635999..eccd61be 100644 Binary files a/public/static/logos/legacy/arch-legacy-ribbon2.png and b/public/static/logos/legacy/arch-legacy-ribbon2.png differ diff --git a/public/static/logos/legacy/arch-legacy-ribbon3.png b/public/static/logos/legacy/arch-legacy-ribbon3.png index c3c00b85..df412335 100644 Binary files a/public/static/logos/legacy/arch-legacy-ribbon3.png and b/public/static/logos/legacy/arch-legacy-ribbon3.png differ diff --git a/public/static/logos/legacy/arch-legacy-ribbon4.png b/public/static/logos/legacy/arch-legacy-ribbon4.png index 33a78edf..8f0ed3a0 100644 Binary files a/public/static/logos/legacy/arch-legacy-ribbon4.png and b/public/static/logos/legacy/arch-legacy-ribbon4.png differ diff --git a/public/static/logos/legacy/arch-legacy-ribbon5.png b/public/static/logos/legacy/arch-legacy-ribbon5.png index abf7cce4..e48dc537 100644 Binary files a/public/static/logos/legacy/arch-legacy-ribbon5.png and b/public/static/logos/legacy/arch-legacy-ribbon5.png differ diff --git a/public/static/logos/legacy/arch-legacy-ribbon6.png b/public/static/logos/legacy/arch-legacy-ribbon6.png index 9f275f22..c3091240 100644 Binary files a/public/static/logos/legacy/arch-legacy-ribbon6.png and b/public/static/logos/legacy/arch-legacy-ribbon6.png differ diff --git a/public/static/logos/legacy/arch-legacy-wombat-lg.png b/public/static/logos/legacy/arch-legacy-wombat-lg.png index 0661b6f5..661ec0a9 100644 Binary files a/public/static/logos/legacy/arch-legacy-wombat-lg.png and b/public/static/logos/legacy/arch-legacy-wombat-lg.png differ diff --git a/public/static/logos/legacy/arch-legacy-wombat.png b/public/static/logos/legacy/arch-legacy-wombat.png index 67e1afac..52678145 100644 Binary files a/public/static/logos/legacy/arch-legacy-wombat.png and b/public/static/logos/legacy/arch-legacy-wombat.png differ diff --git a/retro/static/2007/logo.png b/retro/static/2007/logo.png index b2b6d863..44968cd1 100644 Binary files a/retro/static/2007/logo.png and b/retro/static/2007/logo.png differ diff --git a/retro/static/2008/logo.png b/retro/static/2008/logo.png index b2b6d863..44968cd1 100644 Binary files a/retro/static/2008/logo.png and b/retro/static/2008/logo.png differ diff --git a/retro/static/2009/sevenl_button.png b/retro/static/2009/sevenl_button.png index 131b4dc8..93adcdf0 100644 Binary files a/retro/static/2009/sevenl_button.png and b/retro/static/2009/sevenl_button.png differ -- cgit v1.2.3-54-g00ecf From b82c7b6a3a0f5b42990ec017bf91e66f71f9bfe2 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 31 Jan 2013 13:01:27 -0600 Subject: Remove some whitespace from index template We had a lot going on here in the news section as far as Django template tags go, so remove some whitespace to prevent so many empty lines from being ommitted. This doesn't remove all of it from the generated HTML, but does cut it down significantly. Signed-off-by: Dan McGee --- templates/public/index.html | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/templates/public/index.html b/templates/public/index.html index 6fc90436..1a1a8f2d 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -45,8 +45,7 @@

        RSS Feed - {% for news in news_updates %} - {% if forloop.counter0 < 5 %} + {% for news in news_updates %}{% if forloop.counter0 < 5 %}

        {{ news.title }} @@ -56,8 +55,7 @@

        {% if forloop.counter0 == 0 %}{{ news.html|truncatewords_html:300 }} {% else %}{{ news.html|truncatewords_html:100 }}{% endif %}

    - {% else %} - {% if forloop.counter0 == 5 %} + {% else %}{% if forloop.counter0 == 5 %}

    Older News @@ -70,11 +68,8 @@

    {{ news.title }} - {% if forloop.last %} - - {% endif %} - {% endif %} - {% endfor %} + {% if forloop.last %}{% endif %} + {% endif %}{% endfor %} {% endcache %} {% endblock %} -- cgit v1.2.3-54-g00ecf From 26d6fba089f525505be4ee751fd8a4d37961cad0 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 3 Feb 2013 13:29:13 -0600 Subject: Fix spacing issues in signoffs 'Show More' links When we had a simple multi-line message here, we would end up with too much spacing wherever the link had planted itself due to the div adding visual whitespace. Remove the div completely when the link is clicked to remedy this. Signed-off-by: Dan McGee --- sitestatic/archweb.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sitestatic/archweb.js b/sitestatic/archweb.js index 4a02fb63..dda22d9e 100644 --- a/sitestatic/archweb.js +++ b/sitestatic/archweb.js @@ -451,15 +451,17 @@ function collapseNotes(elements) { return; } contents.slice(maxElements).wrapAll('
    '); - ele.append('
    Show More…'); + ele.append('Show More…'); // add link and wire it up to show the hidden items ele.find('a.morelink').click(function(event) { event.preventDefault(); - ele.find('div.hide').show(); $(this).remove(); - // remove trailing line break between text and our link - $(this).contents().last().filter('br').remove(); + ele.find('br.morelink-spacer').remove(); + // move the div contents back and delete the empty div + var hidden = ele.find('div.hide'); + hidden.contents().appendTo(ele); + hidden.remove(); }); }); } -- cgit v1.2.3-54-g00ecf From 844ed8109317db882e5d2b2ae8fa6084f794d798 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 3 Feb 2013 13:39:08 -0600 Subject: Move the home page script block further down the page Put it after every bit of HTML content, including the page footer. Right now this was the only page using this block in the main template; we should move some other pages using a lot of JS to this format as well. Signed-off-by: Dan McGee --- templates/base.html | 2 +- templates/public/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/base.html b/templates/base.html index c6aa3f17..cc507fbf 100644 --- a/templates/base.html +++ b/templates/base.html @@ -68,7 +68,6 @@ {% block content_right %}{% endblock %}
    {% endblock %} - {% block content_after %}{% endblock %} + {% block script_block %}{% endblock %} diff --git a/templates/public/index.html b/templates/public/index.html index 1a1a8f2d..8926a061 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -208,7 +208,7 @@

    More Resources

    {% endcache %} {% endblock %} -{% block content_after %} +{% block script_block %} {% load cdn %}{% jquery %} -- cgit v1.2.3-54-g00ecf From 9da8a63dd476fe3607a68a028655c9f9d0fee163 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 3 Feb 2013 13:55:25 -0600 Subject: Add DeveloperKey model We're starting to see developers use subkeys of their primary key to sign packages, which we aren't handling well in the web interface. These subkeys show up as unknown, which isn't strictly true. Start the process of being able to handle these keys by adding a model that will store all known keys and subkeys and the relationships among them, as well as which developer owns each. Signed-off-by: Dan McGee --- devel/admin.py | 12 ++- devel/migrations/0009_auto__add_developerkey.py | 126 ++++++++++++++++++++++++ devel/models.py | 15 ++- 3 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 devel/migrations/0009_auto__add_developerkey.py diff --git a/devel/admin.py b/devel/admin.py index 5a704c0b..971933b7 100644 --- a/devel/admin.py +++ b/devel/admin.py @@ -2,7 +2,7 @@ from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User -from .models import UserProfile, MasterKey, PGPSignature +from .models import UserProfile, MasterKey, DeveloperKey, PGPSignature class UserProfileInline(admin.StackedInline): @@ -17,7 +17,14 @@ class UserProfileAdmin(UserAdmin): class MasterKeyAdmin(admin.ModelAdmin): list_display = ('pgp_key', 'owner', 'created', 'revoker', 'revoked') - search_fields = ('pgp_key', 'owner', 'revoker') + search_fields = ('pgp_key', 'owner__username', 'revoker__username') + date_hierarchy = 'created' + + +class DeveloperKeyAdmin(admin.ModelAdmin): + list_display = ('key', 'parent', 'owner', 'created', 'expires', 'revoked') + search_fields = ('key', 'owner__username') + list_filter = ('owner',) date_hierarchy = 'created' @@ -32,6 +39,7 @@ class PGPSignatureAdmin(admin.ModelAdmin): admin.site.register(User, UserProfileAdmin) admin.site.register(MasterKey, MasterKeyAdmin) +admin.site.register(DeveloperKey, DeveloperKeyAdmin) admin.site.register(PGPSignature, PGPSignatureAdmin) # vim: set ts=4 sw=4 et: diff --git a/devel/migrations/0009_auto__add_developerkey.py b/devel/migrations/0009_auto__add_developerkey.py new file mode 100644 index 00000000..60d3f7b8 --- /dev/null +++ b/devel/migrations/0009_auto__add_developerkey.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.create_table('devel_developerkey', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('owner', self.gf('django.db.models.fields.related.ForeignKey')(related_name='all_keys', null=True, to=orm['auth.User'])), + ('key', self.gf('devel.fields.PGPKeyField')(unique=True, max_length=40)), + ('created', self.gf('django.db.models.fields.DateTimeField')()), + ('expires', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('revoked', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['devel.DeveloperKey'], null=True, on_delete=models.SET_NULL)), + )) + db.send_create_signal('devel', ['DeveloperKey']) + + def backwards(self, orm): + db.delete_table('devel_developerkey') + + + 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'}) + }, + 'devel.developerkey': { + 'Meta': {'object_name': 'DeveloperKey'}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('devel.fields.PGPKeyField', [], {'unique': 'True', 'max_length': '40'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'all_keys'", 'null': 'True', 'to': "orm['auth.User']"}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['devel.DeveloperKey']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'devel.masterkey': { + 'Meta': {'ordering': "('created',)", 'object_name': 'MasterKey'}, + 'created': ('django.db.models.fields.DateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'masterkey_owner'", 'to': "orm['auth.User']"}), + 'pgp_key': ('devel.fields.PGPKeyField', [], {'max_length': '40'}), + 'revoked': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'revoker': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'masterkey_revoker'", 'to': "orm['auth.User']"}) + }, + 'devel.pgpsignature': { + 'Meta': {'ordering': "('signer', 'signee')", 'object_name': 'PGPSignature'}, + 'created': ('django.db.models.fields.DateField', [], {}), + 'expires': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'signee': ('devel.fields.PGPKeyField', [], {'max_length': '40'}), + 'signer': ('devel.fields.PGPKeyField', [], {'max_length': '40'}), + 'valid': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'devel.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'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', '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'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {}), + 'latin_name': ('django.db.models.fields.CharField', [], {'max_length': '255', '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'}), + 'pgp_key': ('devel.fields.PGPKeyField', [], {'max_length': '40', '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'}) + }, + 'main.repo': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Repo', 'db_table': "'repos'"}, + 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}), + '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'}) + } + } + + complete_apps = ['devel'] diff --git a/devel/models.py b/devel/models.py index 6689ca3d..67de40a6 100644 --- a/devel/models.py +++ b/devel/models.py @@ -68,7 +68,6 @@ def get_absolute_url(self): return '/%s/#%s' % (prefix, self.user.username) - class MasterKey(models.Model): owner = models.ForeignKey(User, related_name='masterkey_owner', help_text="The developer holding this master key") @@ -88,6 +87,20 @@ def __unicode__(self): self.owner.get_full_name(), self.created) +class DeveloperKey(models.Model): + owner = models.ForeignKey(User, related_name='all_keys', null=True, + help_text="The developer this key belongs to") + key = PGPKeyField(max_length=40, verbose_name="PGP key fingerprint", + unique=True) + created = models.DateTimeField() + expires = models.DateTimeField(null=True, blank=True) + revoked = models.DateTimeField(null=True, blank=True) + parent = models.ForeignKey('self', null=True, on_delete=models.SET_NULL) + + def __unicode__(self): + return self.key + + class PGPSignature(models.Model): signer = PGPKeyField(max_length=40, verbose_name="Signer key fingerprint") signee = PGPKeyField(max_length=40, verbose_name="Signee key fingerprint") -- cgit v1.2.3-54-g00ecf From 7e6279057a57ef44c11349e594ad392fbfce0098 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 3 Feb 2013 14:26:10 -0600 Subject: Add new pgp_import command; replaces import_signatures This command now imports keys, subkeys, and signatures of those keys & subkeys. This will allow us to actually match developers with their packages signed by subkeys rather than the primary key. Signed-off-by: Dan McGee --- devel/management/commands/import_signatures.py | 123 ------------- devel/management/commands/pgp_import.py | 241 +++++++++++++++++++++++++ 2 files changed, 241 insertions(+), 123 deletions(-) delete mode 100644 devel/management/commands/import_signatures.py create mode 100644 devel/management/commands/pgp_import.py diff --git a/devel/management/commands/import_signatures.py b/devel/management/commands/import_signatures.py deleted file mode 100644 index da1397ca..00000000 --- a/devel/management/commands/import_signatures.py +++ /dev/null @@ -1,123 +0,0 @@ -# -*- coding: utf-8 -*- -""" -import_signatures command - -Import signatures from a given GPG keyring. - -Usage: ./manage.py generate_keyring -""" - -from collections import namedtuple -from datetime import datetime -import logging -import subprocess -import sys - -from django.core.management.base import BaseCommand, CommandError -from django.db import transaction - -from devel.models import PGPSignature - -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s -> %(levelname)s: %(message)s', - datefmt='%Y-%m-%d %H:%M:%S', - stream=sys.stderr) -logger = logging.getLogger() - -class Command(BaseCommand): - args = "" - help = "Import signatures from a given GPG keyring." - - def handle(self, *args, **options): - v = int(options.get('verbosity', None)) - if v == 0: - logger.level = logging.ERROR - elif v == 1: - logger.level = logging.INFO - elif v == 2: - logger.level = logging.DEBUG - - if len(args) < 1: - raise CommandError("keyring_path must be provided") - - import_signatures(args[0]) - - -SignatureData = namedtuple('SignatureData', - ('signer', 'signee', 'created', 'expires', 'valid')) - - -def get_date(epoch_string): - '''Convert a epoch string into a python 'date' object (not datetime).''' - return datetime.utcfromtimestamp(int(epoch_string)).date() - - -def parse_sigdata(data): - nodes = {} - edges = [] - current_pubkey = None - - # parse all of the output from our successful GPG command - logger.info("parsing command output") - for line in data.split('\n'): - parts = line.split(':') - if parts[0] == 'pub': - current_pubkey = parts[4] - nodes[current_pubkey] = None - if parts[0] == 'uid': - uid = parts[9] - # only set uid if this is the first one encountered - if nodes[current_pubkey] is None: - nodes[current_pubkey] = uid - if parts[0] == 'sig': - signer = parts[4] - created = get_date(parts[5]) - expires = None - if parts[6]: - expires = get_date(parts[6]) - valid = parts[1] != '-' - edge = SignatureData(signer, current_pubkey, - created, expires, valid) - edges.append(edge) - - return nodes, edges - - -def import_signatures(keyring): - gpg_cmd = ["gpg", "--no-default-keyring", "--keyring", keyring, - "--list-sigs", "--with-colons", "--fixed-list-mode"] - logger.info("running command: %r", gpg_cmd) - proc = subprocess.Popen(gpg_cmd, stdout=subprocess.PIPE) - outdata, errdata = proc.communicate() - if proc.returncode != 0: - logger.error(errdata) - raise subprocess.CalledProcessError(proc.returncode, gpg_cmd) - - nodes, edges = parse_sigdata(outdata) - - # now prune the data down to what we actually want. - # prune edges not in nodes, remove duplicates, and self-sigs - pruned_edges = {edge for edge in edges - if edge.signer in nodes and edge.signer != edge.signee} - - logger.info("creating or finding %d signatures", len(pruned_edges)) - created_ct = updated_ct = 0 - with transaction.commit_on_success(): - for edge in pruned_edges: - sig, created = PGPSignature.objects.get_or_create( - signer=edge.signer, signee=edge.signee, - created=edge.created, expires=edge.expires, - defaults={ 'valid': edge.valid }) - if sig.valid != edge.valid: - sig.valid = edge.valid - sig.save() - updated_ct = 1 - if created: - created_ct += 1 - - sig_ct = PGPSignature.objects.all().count() - logger.info("%d total signatures in database", sig_ct) - logger.info("created %d, updated %d signatures", created_ct, updated_ct) - -# vim: set ts=4 sw=4 et: diff --git a/devel/management/commands/pgp_import.py b/devel/management/commands/pgp_import.py new file mode 100644 index 00000000..10e6cfcb --- /dev/null +++ b/devel/management/commands/pgp_import.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- +""" +pgp_import command + +Import keys and signatures from a given GPG keyring. + +Usage: ./manage.py pgp_import +""" + +from collections import namedtuple, OrderedDict +from datetime import datetime +import logging +from pytz import utc +import subprocess +import sys + +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction + +from devel.models import DeveloperKey, PGPSignature +from devel.utils import UserFinder + + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s -> %(levelname)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + stream=sys.stderr) +logger = logging.getLogger() + +class Command(BaseCommand): + args = "" + help = "Import keys and signatures from a given GPG keyring." + + def handle(self, *args, **options): + v = int(options.get('verbosity', None)) + if v == 0: + logger.level = logging.ERROR + elif v == 1: + logger.level = logging.INFO + elif v == 2: + logger.level = logging.DEBUG + + if len(args) < 1: + raise CommandError("keyring_path must be provided") + + import_keys(args[0]) + import_signatures(args[0]) + + +def get_date(epoch_string): + '''Convert a epoch string into a python 'date' object (not datetime).''' + if not epoch_string: + return None + return datetime.utcfromtimestamp(int(epoch_string)).date() + + +def get_datetime(epoch_string): + '''Convert a epoch string into a python 'datetime' object.''' + if not epoch_string: + return None + return datetime.utcfromtimestamp(int(epoch_string)).replace(tzinfo=utc) + + +def call_gpg(keyring, *args): + # GPG is stupid and interprets any filename without path portion as being + # in ~/.gnupg/. Fake it out if we just get a bare filename. + if '/' not in keyring: + keyring = './%s' % keyring + gpg_cmd = ["gpg2", "--no-default-keyring", "--keyring", keyring, + "--with-colons", "--fixed-list-mode"] + gpg_cmd.extend(args) + logger.info("running command: %s", ' '.join(gpg_cmd)) + proc = subprocess.Popen(gpg_cmd, stdout=subprocess.PIPE) + outdata, errdata = proc.communicate() + if proc.returncode != 0: + logger.error(errdata) + raise subprocess.CalledProcessError(proc.returncode, gpg_cmd) + return outdata + + +class KeyData(object): + def __init__(self, key, created, expires): + self.key = key + self.created = get_datetime(created) + self.expires = get_datetime(expires) + self.parent = None + self.revoked = None + self.db_id = None + + +def parse_keydata(data): + keys = OrderedDict() + current_pubkey = None + + # parse all of the output from our successful GPG command + logger.info("parsing command output") + for line in data.split('\n'): + parts = line.split(':') + if parts[0] == 'pub': + key = parts[4] + current_pubkey = key + keys[key] = KeyData(key, parts[5], parts[6]) + node = parts[0] + elif parts[0] == 'sub': + key = parts[4] + keys[key] = KeyData(key, parts[5], parts[6]) + keys[key].parent = current_pubkey + node = parts[0] + elif parts[0] == 'uid': + node = parts[0] + elif parts[0] == 'rev' and node in ('pub', 'sub'): + keys[current_pubkey].revoked = get_datetime(parts[5]) + + return keys + + +def find_key_owner(key, keys, finder): + '''Recurse up the chain, looking for an owner.''' + if key is None: + return None + owner = finder.find_by_pgp_key(key.key) + if owner: + return owner + if key.parent: + return find_key_owner(keys[key.parent], keys, finder) + return None + + +def import_keys(keyring): + outdata = call_gpg(keyring, "--list-sigs") + keydata = parse_keydata(outdata) + + logger.info("creating or finding %d keys", len(keydata)) + created_ct = updated_ct = 0 + with transaction.commit_on_success(): + finder = UserFinder() + # we are dependent on parents coming before children; parse_keydata + # uses an OrderedDict to ensure this is the case. + for data in keydata.values(): + parent_id = None + if data.parent: + parent_data = keydata.get(data.parent, None) + if parent_data: + parent_id = parent_data.db_id + other = { + 'expires': data.expires, + 'revoked': data.revoked, + 'parent_id': parent_id, + } + dkey, created = DeveloperKey.objects.get_or_create( + key=data.key, created=data.created, defaults=other) + data.db_id = dkey.id + + # set or update any additional data we might need to + needs_save = False + if created: + created_ct += 1 + else: + for k, v in other.items(): + if getattr(dkey, k) != v: + setattr(dkey, k, v) + needs_save = True + if dkey.owner_id is None: + owner = find_key_owner(data, keydata, finder) + if owner is not None: + dkey.owner = owner + needs_save = True + if needs_save: + dkey.save() + updated_ct += 1 + + key_ct = DeveloperKey.objects.all().count() + logger.info("%d total keys in database", key_ct) + logger.info("created %d, updated %d keys", created_ct, updated_ct) + + +SignatureData = namedtuple('SignatureData', + ('signer', 'signee', 'created', 'expires', 'valid')) + + +def parse_sigdata(data): + nodes = {} + edges = [] + current_pubkey = None + + # parse all of the output from our successful GPG command + logger.info("parsing command output") + for line in data.split('\n'): + parts = line.split(':') + if parts[0] == 'pub': + current_pubkey = parts[4] + nodes[current_pubkey] = None + if parts[0] == 'uid': + uid = parts[9] + # only set uid if this is the first one encountered + if nodes[current_pubkey] is None: + nodes[current_pubkey] = uid + if parts[0] == 'sig': + signer = parts[4] + created = get_date(parts[5]) + expires = None + if parts[6]: + expires = get_date(parts[6]) + valid = parts[1] != '-' + edge = SignatureData(signer, current_pubkey, + created, expires, valid) + edges.append(edge) + + return nodes, edges + + +def import_signatures(keyring): + outdata = call_gpg(keyring, "--list-sigs") + nodes, edges = parse_sigdata(outdata) + + # now prune the data down to what we actually want. + # prune edges not in nodes, remove duplicates, and self-sigs + pruned_edges = {edge for edge in edges + if edge.signer in nodes and edge.signer != edge.signee} + + logger.info("creating or finding %d signatures", len(pruned_edges)) + created_ct = updated_ct = 0 + with transaction.commit_on_success(): + for edge in pruned_edges: + sig, created = PGPSignature.objects.get_or_create( + signer=edge.signer, signee=edge.signee, + created=edge.created, expires=edge.expires, + defaults={ 'valid': edge.valid }) + if sig.valid != edge.valid: + sig.valid = edge.valid + sig.save() + updated_ct = 1 + if created: + created_ct += 1 + + sig_ct = PGPSignature.objects.all().count() + logger.info("%d total signatures in database", sig_ct) + logger.info("created %d, updated %d signatures", created_ct, updated_ct) + +# vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From d63a800348f81823f157ec9ed03f9985308c3ea3 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 3 Feb 2013 14:40:41 -0600 Subject: Update user_pgp_key_link template tag to use DeveloperKey model The first of several small updates to use the new data we have available. Signed-off-by: Dan McGee --- main/templatetags/pgp.py | 17 +++++++++++------ templates/public/keys.html | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/main/templatetags/pgp.py b/main/templatetags/pgp.py index 581282b4..afad9df2 100644 --- a/main/templatetags/pgp.py +++ b/main/templatetags/pgp.py @@ -3,8 +3,11 @@ from django.utils.html import conditional_escape from django.utils.safestring import mark_safe +from devel.models import DeveloperKey + register = template.Library() + def format_key(key_id): if len(key_id) in (8, 20): return u'0x%s' % key_id @@ -40,12 +43,14 @@ def pgp_key_link(key_id, link_text=None): return '
    %s' % values @register.simple_tag -def user_pgp_key_link(users, key_id): - matched = [user for user in users if user.userprofile.pgp_key and - user.userprofile.pgp_key[-16:] == key_id[-16:]] - if matched and len(matched) == 1: - return pgp_key_link(key_id, matched[0].get_full_name()) - return pgp_key_link(key_id) +def user_pgp_key_link(key_id): + normalized = key_id[-16:] + try: + matching_key = DeveloperKey.objects.select_related( + 'owner').get(key=normalized, owner_id__isnull=False) + except DeveloperKey.DoesNotExist: + return pgp_key_link(key_id) + return pgp_key_link(key_id, matching_key.owner.get_full_name()) @register.filter(needs_autoescape=True) diff --git a/templates/public/keys.html b/templates/public/keys.html index 35bf414c..cf8ac784 100644 --- a/templates/public/keys.html +++ b/templates/public/keys.html @@ -124,8 +124,8 @@

    Developer Cross-Signatures

    {% for sig in cross_signatures %} - {% user_pgp_key_link active_users sig.signer %} - {% user_pgp_key_link active_users sig.signee %} + {% user_pgp_key_link sig.signer %} + {% user_pgp_key_link sig.signee %} {{ sig.created }} {{ sig.expires|default:"" }} -- cgit v1.2.3-54-g00ecf From 508d06af810c787b2644331444279407ccfa27af Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 3 Feb 2013 15:08:21 -0600 Subject: Use DeveloperKey model on package page and reports This introduces the new model to the package page so subkey signings show up as attributed to the original developer. We also teach the mismatched signatures report to recognize all keys and subkeys of a given developer, cutting down on some of the bogus results. Signed-off-by: Dan McGee --- devel/views.py | 33 +++++++++++++++++++++++---------- main/models.py | 6 ++++-- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/devel/views.py b/devel/views.py index 90839847..ff1dec12 100644 --- a/devel/views.py +++ b/devel/views.py @@ -20,6 +20,7 @@ from django.utils.timezone import now from .forms import ProfileForm, UserProfileForm, NewUserForm +from .models import DeveloperKey from main.models import Package, PackageFile from main.models import Arch, Repo from news.models import News @@ -27,7 +28,7 @@ from packages.utils import get_signoff_groups from todolists.models import TodolistPackage from todolists.utils import get_annotated_todolists -from .utils import get_annotated_maintainers, UserFinder +from .utils import get_annotated_maintainers @login_required @@ -262,18 +263,30 @@ def report(request, report_name, username=None): names = [ 'Signature Date', 'Signed By', 'Packager' ] attrs = [ 'sig_date', 'sig_by', 'packager' ] cutoff = timedelta(hours=24) - finder = UserFinder() filtered = [] - packages = packages.filter(pgp_signature__isnull=False) + packages = packages.select_related( + 'arch', 'repo', 'packager').filter(pgp_signature__isnull=False) + known_keys = DeveloperKey.objects.select_related( + 'owner').filter(owner__isnull=False) + known_keys = {dk.key: dk for dk in known_keys} for package in packages: - sig_date = package.signature.creation_time.replace(tzinfo=pytz.utc) + bad = False + sig = package.signature + sig_date = sig.creation_time.replace(tzinfo=pytz.utc) package.sig_date = sig_date.date() - key_id = package.signature.key_id - signer = finder.find_by_pgp_key(key_id) - package.sig_by = signer or key_id - if signer is None or signer.id != package.packager_id: - filtered.append(package) - elif sig_date > package.build_date + cutoff: + dev_key = known_keys.get(sig.key_id, None) + if dev_key: + package.sig_by = dev_key.owner + if dev_key.owner_id != package.packager_id: + bad = True + else: + package.sig_by = sig.key_id + bad = True + + if sig_date > package.build_date + cutoff: + bad = True + + if bad: filtered.append(package) packages = filtered else: diff --git a/main/models.py b/main/models.py index 40466d65..53a24ffc 100644 --- a/main/models.py +++ b/main/models.py @@ -11,6 +11,7 @@ from .fields import PositiveBigIntegerField from .utils import cache_function, set_created_field +from devel.models import DeveloperKey from packages.alpm import AlpmAPI @@ -153,8 +154,9 @@ def signer(self): sig = self.signature if sig and sig.key_id: try: - user = User.objects.get( - userprofile__pgp_key__endswith=sig.key_id) + matching_key = DeveloperKey.objects.select_related( + 'owner').get(key=sig.key_id, owner_id__isnull=False) + user = matching_key.owner except User.DoesNotExist: user = None return user -- cgit v1.2.3-54-g00ecf From cfa2798880eccda63a3ed4d3eddadeb01f5065d2 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 4 Feb 2013 17:01:58 -0600 Subject: Update exception to DeveloperKey.DoesNotExist We aren't looking up users; we are looking up developer keys. Signed-off-by: Dan McGee --- main/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/models.py b/main/models.py index 53a24ffc..46fd3a63 100644 --- a/main/models.py +++ b/main/models.py @@ -157,7 +157,7 @@ def signer(self): matching_key = DeveloperKey.objects.select_related( 'owner').get(key=sig.key_id, owner_id__isnull=False) user = matching_key.owner - except User.DoesNotExist: + except DeveloperKey.DoesNotExist: user = None return user return None -- cgit v1.2.3-54-g00ecf From f98ff8cd22185c11dccdbe19b5bb7ed849b38e6b Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 4 Feb 2013 20:02:00 -0600 Subject: Add './' hack to generate_keyring as well If you specify a relative path to gpg without a slash character, it interprets as relative to ~/.gnupg, which is stupid. Signed-off-by: Dan McGee --- devel/management/commands/generate_keyring.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/devel/management/commands/generate_keyring.py b/devel/management/commands/generate_keyring.py index 15ae488d..34bcd2f8 100644 --- a/devel/management/commands/generate_keyring.py +++ b/devel/management/commands/generate_keyring.py @@ -55,6 +55,10 @@ def generate_keyring(keyserver, keyring): master_key_ids = MasterKey.objects.values_list("pgp_key", flat=True) logger.info("%d keys fetched from master keys", len(master_key_ids)) + # GPG is stupid and interprets any filename without path portion as being + # in ~/.gnupg/. Fake it out if we just get a bare filename. + if '/' not in keyring: + keyring = './%s' % keyring gpg_cmd = ["gpg", "--no-default-keyring", "--keyring", keyring, "--keyserver", keyserver, "--recv-keys"] logger.info("running command: %r", gpg_cmd) -- cgit v1.2.3-54-g00ecf From 8d79a1ea84756b016fb76d940e95a8885d014dae Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 8 Feb 2013 21:03:52 -0600 Subject: Minify static files when running collectstatic This doesn't do any super optimizations, but does run the very basic cssmin and jsmin Python tools over the static resources we serve up. Signed-off-by: Dan McGee --- main/storage.py | 36 ++++++++++++++++++++++++++++++++++++ requirements.txt | 2 ++ requirements_prod.txt | 2 ++ settings.py | 2 +- 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 main/storage.py diff --git a/main/storage.py b/main/storage.py new file mode 100644 index 00000000..62e94ef7 --- /dev/null +++ b/main/storage.py @@ -0,0 +1,36 @@ +import cssmin +import jsmin + +from django.contrib.staticfiles.storage import CachedStaticFilesStorage +from django.core.files.base import ContentFile +from django.utils.encoding import smart_str + + +class MinifiedStaticFilesStorage(CachedStaticFilesStorage): + """ + A static file system storage backend which minifies the hashed + copies of the files it saves. It currently knows how to process + CSS and JS files. Files containing '.min' anywhere in the filename + are skipped as they are already assumed minified. + """ + minifiers = ( + ('.css', cssmin.cssmin), + ('.js', jsmin.jsmin), + ) + + def post_process(self, paths, dry_run=False, **options): + for original_path, processed_path, processed in super( + MinifiedStaticFilesStorage, self).post_process( + paths, dry_run, **options): + for ext, func in self.minifiers: + if '.min' in original_path: + continue + if original_path.endswith(ext): + with self._open(processed_path) as processed_file: + minified = func(processed_file.read()) + minified_file = ContentFile(smart_str(minified)) + self.delete(processed_path) + self._save(processed_path, minified_file) + processed = True + + yield original_path, processed_path, processed diff --git a/requirements.txt b/requirements.txt index ae58ee58..b8a06215 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,9 @@ +-e git+git://github.com/fredj/cssmin.git@master#egg=cssmin Django==1.4.3 Markdown==2.2.1 South==0.7.6 bencode==1.0 django-countries==1.5 +jsmin==2.0.2-1 pgpdump==1.4 pytz>=2012j diff --git a/requirements_prod.txt b/requirements_prod.txt index ee1b17ad..705c56bb 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -1,8 +1,10 @@ +-e git+git://github.com/fredj/cssmin.git@master#egg=cssmin Django==1.4.3 Markdown==2.2.1 South==0.7.6 bencode==1.0 django-countries==1.5 +jsmin==2.0.2-1 pgpdump==1.4 psycopg2==2.4.6 pyinotify==0.9.4 diff --git a/settings.py b/settings.py index c856bf57..559a55a0 100644 --- a/settings.py +++ b/settings.py @@ -90,7 +90,7 @@ ) # Static files backend that allows us to use far future Expires headers -STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.CachedStaticFilesStorage' +STATICFILES_STORAGE = 'main.storage.MinifiedStaticFilesStorage' # Configure where messages should reside MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' -- cgit v1.2.3-54-g00ecf From e65c7805547484cad1be55dfa20355ef18b857be Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 8 Feb 2013 21:09:47 -0600 Subject: Remove package seach by 'Last Updated After' It is a lot easier to just sort the list rather than mess with this particular field, which didn't even allow you to specify a range or direction to search in. Signed-off-by: Dan McGee --- packages/views/search.py | 9 --------- templates/packages/search.html | 14 -------------- 2 files changed, 23 deletions(-) diff --git a/packages/views/search.py b/packages/views/search.py index 0f313ccb..9cb5f38d 100644 --- a/packages/views/search.py +++ b/packages/views/search.py @@ -3,7 +3,6 @@ from pytz import utc from django import forms -from django.contrib.admin.widgets import AdminDateWidget from django.contrib.auth.models import User from django.db.models import Q from django.http import HttpResponse @@ -44,8 +43,6 @@ class PackageSearchForm(forms.Form): sort = forms.CharField(required=False, widget=forms.HiddenInput()) maintainer = forms.ChoiceField(required=False) packager = forms.ChoiceField(required=False) - last_update = forms.DateField(required=False, widget=AdminDateWidget(), - label='Last Updated After') flagged = forms.ChoiceField( choices=[('', 'All')] + make_choice(['Flagged', 'Not Flagged']), required=False) @@ -104,12 +101,6 @@ def parse_form(form, packages): elif form.cleaned_data['flagged'] == 'Not Flagged': packages = packages.filter(flag_date__isnull=True) - if form.cleaned_data['last_update']: - lu = form.cleaned_data['last_update'] - cutoff = datetime(lu.year, lu.month, lu.day, 0, 0) - cutoff = cutoff.replace(tzinfo=utc) - packages = packages.filter(last_update__gte=cutoff) - if form.cleaned_data['name']: name = form.cleaned_data['name'] packages = packages.filter(pkgname=name) diff --git a/templates/packages/search.html b/templates/packages/search.html index ab9b6d32..a5d52d6c 100644 --- a/templates/packages/search.html +++ b/templates/packages/search.html @@ -1,13 +1,11 @@ {% extends "base.html" %} {% load package_extras %} -{% load admin_static %} {% block title %}Arch Linux - Package Database{% endblock %} {% block navbarclass %}anb-packages{% endblock %} {% block head %} {% if is_paginated and page_obj.number > 1 %}{% endif %} - {% endblock %} @@ -35,9 +33,6 @@

    Package Search

    {{ search_form.maintainer.errors }} {{ search_form.maintainer}}
    -
    {{ search_form.last_update.errors }} - {{ search_form.last_update }}
    {{ search_form.flagged.errors }} {{ search_form.flagged }}
    @@ -126,13 +121,4 @@

    Package Search

    For unsupported packages, browse the Arch User Repository (AUR).

    - -{% load cdn %}{% jquery %} - - - -{{search_form.media}} {% endblock %} -- cgit v1.2.3-54-g00ecf From 445c242dac8f7c94f5d689fa3597ac573a4863a4 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 4 Feb 2013 23:27:44 -0600 Subject: Add a releases model sitemap Signed-off-by: Dan McGee --- sitemaps.py | 17 +++++++++++++++++ urls.py | 1 + 2 files changed, 18 insertions(+) diff --git a/sitemaps.py b/sitemaps.py index 110c588a..b079d8b7 100644 --- a/sitemaps.py +++ b/sitemaps.py @@ -7,6 +7,8 @@ from main.models import Package from news.models import News from packages.utils import get_group_info, get_split_packages_info +from releng.models import Release + class PackagesSitemap(Sitemap): changefreq = "weekly" @@ -86,6 +88,21 @@ def changefreq(self, obj): return 'yearly' +class ReleasesSitemap(Sitemap): + changefreq = "monthly" + + def items(self): + return Release.objects.all().defer('info', 'torrent_data').order_by() + + def lastmod(self, obj): + return obj.created + + def priority(self, obj): + if obj.available: + return "0.6" + return "0.2" + + class BaseSitemap(Sitemap): DEFAULT_PRIORITY = 0.7 diff --git a/urls.py b/urls.py index 97b84a36..e53d0012 100644 --- a/urls.py +++ b/urls.py @@ -16,6 +16,7 @@ 'package-files': sitemaps.PackageFilesSitemap, 'package-groups': sitemaps.PackageGroupsSitemap, 'split-packages': sitemaps.SplitPackagesSitemap, + 'releases': sitemaps.ReleasesSitemap, } admin.autodiscover() -- cgit v1.2.3-54-g00ecf From 82947873d65d06d4d938402b57e9244629f97228 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 8 Feb 2013 21:19:17 -0600 Subject: Upgrade D3 to 3.0.6 Signed-off-by: Dan McGee --- templates/mirrors/mirror_details.html | 2 +- templates/public/keys.html | 2 +- templates/visualize/index.html | 2 +- visualize/static/d3-3.0.0.js | 7809 --------------------------------- visualize/static/d3-3.0.0.min.js | 4 - visualize/static/d3-3.0.6.js | 7790 ++++++++++++++++++++++++++++++++ visualize/static/d3-3.0.6.min.js | 4 + 7 files changed, 7797 insertions(+), 7816 deletions(-) delete mode 100644 visualize/static/d3-3.0.0.js delete mode 100644 visualize/static/d3-3.0.0.min.js create mode 100644 visualize/static/d3-3.0.6.js create mode 100644 visualize/static/d3-3.0.6.min.js diff --git a/templates/mirrors/mirror_details.html b/templates/mirrors/mirror_details.html index 8ea6bbec..02d68a3a 100644 --- a/templates/mirrors/mirror_details.html +++ b/templates/mirrors/mirror_details.html @@ -112,7 +112,7 @@

    Mirror Status Chart

    {% load cdn %}{% jquery %}{% jquery_tablesorter %} - + + + - + {% endblock %} -- cgit v1.2.3-54-g00ecf From 55179a4f9e8b80d515bae7032af8aefc33ae0192 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 16 Jan 2013 16:07:26 -0600 Subject: reporead: remove batched_bulk_create Now that Django 1.5 is out and realized SQLite3 only allows for 999 parameters per SQL call, we don't need to manually batch things up anymore and can let the underlying bulk_create code do it for us. This basically reverts commit 88ee61a39ac3. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 32 +++++--------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index ab0efeed..ccac55f2 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -32,7 +32,6 @@ from devel.utils import UserFinder from main.models import Arch, Package, PackageFile, Repo -from main.utils import database_vendor from packages.models import Depend, Conflict, Provision, Replacement, Update from packages.utils import parse_version @@ -182,27 +181,6 @@ def create_related(model, package, rel_str, equals_only=False): return related -def batched_bulk_create(model, all_objects): - # for short lists, just bulk_create as we should be fine - if len(all_objects) < 20: - return model.objects.bulk_create(all_objects) - - if database_vendor(model, mode='write') == 'sqlite': - # 999 max variables in each SQL statement - incr = 999 // len(model._meta.fields) - else: - incr = 1000 - - def chunks(): - offset = 0 - while offset < len(all_objects): - yield all_objects[offset:offset + incr] - offset += incr - - for items in chunks(): - model.objects.bulk_create(items) - - def create_multivalued(dbpkg, repopkg, db_attr, repo_attr): '''Populate the simplest of multivalued attributes. These are those that only deal with a 'name' attribute, such as licenses, groups, etc. The input @@ -256,20 +234,20 @@ def populate_pkg(dbpkg, repopkg, force=False, timestamp=None): deps += [create_depend(dbpkg, y, 'O') for y in repopkg.optdepends] deps += [create_depend(dbpkg, y, 'M') for y in repopkg.makedepends] deps += [create_depend(dbpkg, y, 'C') for y in repopkg.checkdepends] - batched_bulk_create(Depend, deps) + Depend.objects.bulk_create(deps) dbpkg.conflicts.all().delete() conflicts = [create_related(Conflict, dbpkg, y) for y in repopkg.conflicts] - batched_bulk_create(Conflict, conflicts) + Conflict.objects.bulk_create(conflicts) dbpkg.provides.all().delete() provides = [create_related(Provision, dbpkg, y, equals_only=True) for y in repopkg.provides] - batched_bulk_create(Provision, provides) + Provision.objects.bulk_create(provides) dbpkg.replaces.all().delete() replaces = [create_related(Replacement, dbpkg, y) for y in repopkg.replaces] - batched_bulk_create(Replacement, replaces) + Replacement.objects.bulk_create(replaces) create_multivalued(dbpkg, repopkg, 'groups', 'groups') create_multivalued(dbpkg, repopkg, 'licenses', 'license') @@ -319,7 +297,7 @@ def populate_files(dbpkg, repopkg, force=False): directory=dirname, filename=filename) pkg_files.append(pkgfile) - batched_bulk_create(PackageFile, pkg_files) + PackageFile.objects.bulk_create(pkg_files) dbpkg.files_last_update = now() dbpkg.save() -- cgit v1.2.3-54-g00ecf From 10462425f989dc74653179f0845978a6b5e30045 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 16 Jan 2013 16:10:24 -0600 Subject: Remove {% load url from future %} from templates This is now the default in Django 1.5. Signed-off-by: Dan McGee --- templates/base.html | 2 +- templates/packages/removed.html | 1 - templates/public/download.html | 1 - templates/public/index.html | 1 - templates/public/keys.html | 1 - templates/releng/add.html | 1 - templates/releng/iso_overview.html | 1 - templates/releng/release_detail.html | 1 - templates/releng/release_list.html | 1 - templates/releng/result_list.html | 1 - templates/releng/result_section.html | 1 - templates/releng/results.html | 1 - templates/releng/thanks.html | 1 - templates/visualize/index.html | 1 - 14 files changed, 1 insertion(+), 14 deletions(-) diff --git a/templates/base.html b/templates/base.html index cc507fbf..15bb7e28 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,4 +1,4 @@ -{% load url from future %}{% load static from staticfiles %} +{% load static from staticfiles %} diff --git a/templates/packages/removed.html b/templates/packages/removed.html index ea20ce80..f188b6db 100644 --- a/templates/packages/removed.html +++ b/templates/packages/removed.html @@ -1,5 +1,4 @@ {% extends "base.html" %} -{% load url from future %} {% load package_extras %} {% block title %}Arch Linux - Not Available - {{ name }} {{ version }} ({{ arch.name }}){% endblock %} diff --git a/templates/public/download.html b/templates/public/download.html index c68cf66b..7936efb5 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load cache %} -{% load url from future %} {% load static from staticfiles %} {% load flags %} diff --git a/templates/public/index.html b/templates/public/index.html index 8926a061..3f88c183 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load cache %} -{% load url from future %} {% load static from staticfiles %} {% block head %} diff --git a/templates/public/keys.html b/templates/public/keys.html index ad2dd19d..91b53075 100644 --- a/templates/public/keys.html +++ b/templates/public/keys.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load static from staticfiles %} -{% load url from future %} {% load pgp %} {% block title %}Arch Linux - Master Signing Keys{% endblock %} diff --git a/templates/releng/add.html b/templates/releng/add.html index ed02984e..d060395d 100644 --- a/templates/releng/add.html +++ b/templates/releng/add.html @@ -1,5 +1,4 @@ {% extends "base.html" %} -{% load url from future %} {% block title %}Arch Linux - Test Result Entry{% endblock %} diff --git a/templates/releng/iso_overview.html b/templates/releng/iso_overview.html index 23ef0135..196f0c0a 100644 --- a/templates/releng/iso_overview.html +++ b/templates/releng/iso_overview.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load static from staticfiles %} -{% load url from future %} {% block content %}
    diff --git a/templates/releng/release_detail.html b/templates/releng/release_detail.html index 01c0319e..09507536 100644 --- a/templates/releng/release_detail.html +++ b/templates/releng/release_detail.html @@ -1,5 +1,4 @@ {% extends "base.html" %} -{% load url from future %} {% block title %}Arch Linux - Release: {{ release.version }}{% endblock %} diff --git a/templates/releng/release_list.html b/templates/releng/release_list.html index ef53a93d..5f248264 100644 --- a/templates/releng/release_list.html +++ b/templates/releng/release_list.html @@ -1,5 +1,4 @@ {% extends "base.html" %} -{% load url from future %} {% load static from staticfiles %} {% block title %}Arch Linux - Releases{% endblock %} diff --git a/templates/releng/result_list.html b/templates/releng/result_list.html index be5783b3..12bf39db 100644 --- a/templates/releng/result_list.html +++ b/templates/releng/result_list.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load static from staticfiles %} -{% load url from future %} {% block content %}
    diff --git a/templates/releng/result_section.html b/templates/releng/result_section.html index ae951ebe..5e0b6f62 100644 --- a/templates/releng/result_section.html +++ b/templates/releng/result_section.html @@ -1,4 +1,3 @@ -{% load url from future %} {% if option.is_rollback %}Rollback: {% endif %}{{ option.name|title }} Last Success diff --git a/templates/releng/results.html b/templates/releng/results.html index c19b42a6..59d8351d 100644 --- a/templates/releng/results.html +++ b/templates/releng/results.html @@ -1,5 +1,4 @@ {% extends "base.html" %} -{% load url from future %} {% block title %}Arch Linux - Release Engineering Testbuild Results{% endblock %} diff --git a/templates/releng/thanks.html b/templates/releng/thanks.html index 66d65a5c..2462dafd 100644 --- a/templates/releng/thanks.html +++ b/templates/releng/thanks.html @@ -1,5 +1,4 @@ {% extends "base.html" %} -{% load url from future %} {% block title %}Arch Linux - Feedback - Thanks!{% endblock %} diff --git a/templates/visualize/index.html b/templates/visualize/index.html index 1e932d9b..2b79d601 100644 --- a/templates/visualize/index.html +++ b/templates/visualize/index.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load static from staticfiles %} -{% load url from future %} {% block title %}Arch Linux - Visualizations{% endblock %} -- cgit v1.2.3-54-g00ecf From 271d1babbf8038e17d9dc5cfc3cd659463848400 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 20 Jan 2013 12:43:33 -0600 Subject: Revert "Reduce query count when retrieving satisfiers and providers" This reverts commit 20b64e42672d185821cc584dfa4b133ee259a144. Django 1.5 fixed this issue and now parent objects are automatically attached to their children when queries go through the related manager. See "Caching of related model instances" in the release notes. Signed-off-by: Dan McGee --- main/models.py | 4 ++-- packages/models.py | 20 ++++++++------------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/main/models.py b/main/models.py index 46fd3a63..da9d8b6e 100644 --- a/main/models.py +++ b/main/models.py @@ -277,10 +277,10 @@ def get_depends(self): # TODO: we can use list comprehension and an 'in' query to make this # more effective for dep in self.depends.all(): - pkg = dep.get_best_satisfier(self) + pkg = dep.get_best_satisfier() providers = None if not pkg: - providers = dep.get_providers(self) + providers = dep.get_providers() deps.append({'dep': dep, 'pkg': pkg, 'providers': providers}) # sort the list; deptype sorting makes this tricker than expected sort_order = {'D': 0, 'O': 1, 'M': 2, 'C': 3} diff --git a/packages/models.py b/packages/models.py index ff677883..7bcdc000 100644 --- a/packages/models.py +++ b/packages/models.py @@ -357,16 +357,14 @@ class RelatedToBase(models.Model): name = models.CharField(max_length=255, db_index=True) version = models.CharField(max_length=255, default='') - def get_best_satisfier(self, main_pkg=None): + def get_best_satisfier(self): '''Find a satisfier for this related package that best matches the given criteria. It will not search provisions, but will find packages named and matching repo characteristics if possible.''' - if main_pkg is None: - main_pkg = self.pkg pkgs = Package.objects.normal().filter(pkgname=self.name) - if not main_pkg.arch.agnostic: + if not self.pkg.arch.agnostic: # make sure we match architectures if possible - arches = main_pkg.applicable_arches() + arches = self.pkg.applicable_arches() pkgs = pkgs.filter(arch__in=arches) # if we have a comparison operation, make sure the packages we grab # actually satisfy the requirements @@ -386,27 +384,25 @@ def get_best_satisfier(self, main_pkg=None): pkg = pkgs[0] # prevents yet more DB queries, these lists should be short; # after each grab the best available in case we remove all entries - pkgs = [p for p in pkgs if p.repo.staging == main_pkg.repo.staging] + pkgs = [p for p in pkgs if p.repo.staging == self.pkg.repo.staging] if len(pkgs) > 0: pkg = pkgs[0] - pkgs = [p for p in pkgs if p.repo.testing == main_pkg.repo.testing] + pkgs = [p for p in pkgs if p.repo.testing == self.pkg.repo.testing] if len(pkgs) > 0: pkg = pkgs[0] return pkg - def get_providers(self, main_pkg=None): + def get_providers(self): '''Return providers of this related package. Does *not* include exact matches as it checks the Provision names only, use get_best_satisfier() instead for exact matches.''' - if main_pkg is None: - main_pkg = self.pkg pkgs = Package.objects.normal().filter( provides__name=self.name).order_by().distinct() - if not main_pkg.arch.agnostic: + if not self.pkg.arch.agnostic: # make sure we match architectures if possible - arches = main_pkg.applicable_arches() + arches = self.pkg.applicable_arches() pkgs = pkgs.filter(arch__in=arches) # If we have a comparison operation, make sure the packages we grab -- cgit v1.2.3-54-g00ecf From 8524a6057c4b99a620850494a22eb3d1f56bee68 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 20 Jan 2013 14:15:53 -0600 Subject: Change caching strategy on package.applicable_arches Rather than use the Django cache for this (aka memcached), just do it on a per-object instantiation basis. This means no external services calls except the first time to the database, which should be quite a bit faster. Signed-off-by: Dan McGee --- main/models.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/main/models.py b/main/models.py index da9d8b6e..ec5c2bd6 100644 --- a/main/models.py +++ b/main/models.py @@ -177,12 +177,15 @@ def maintainers(self): def maintainers(self, maintainers): self._maintainers = maintainers - @cache_function(1800) + _applicable_arches = None + def applicable_arches(self): '''The list of (this arch) + (available agnostic arches).''' - arches = set(Arch.objects.filter(agnostic=True)) - arches.add(self.arch) - return list(arches) + if self._applicable_arches is None: + arches = set(Arch.objects.filter(agnostic=True)) + arches.add(self.arch) + self._applicable_arches = list(arches) + return self._applicable_arches @cache_function(119) def get_requiredby(self): -- cgit v1.2.3-54-g00ecf From 5566d43a7734f6bb2f48d5d511351da12ddc5cc1 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 9 Feb 2013 16:43:40 -0600 Subject: Use 'update_fields' model.save() kwarg This was added in Django 1.5 and allows saving only a subset of a model's fields. It makes sense in a few cases to utilize it. Signed-off-by: Dan McGee --- mirrors/management/commands/mirrorresolv.py | 2 +- packages/views/signoff.py | 2 +- releng/management/commands/syncisos.py | 2 +- todolists/views.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mirrors/management/commands/mirrorresolv.py b/mirrors/management/commands/mirrorresolv.py index 0370f8ed..a6c2523e 100644 --- a/mirrors/management/commands/mirrorresolv.py +++ b/mirrors/management/commands/mirrorresolv.py @@ -53,7 +53,7 @@ def resolve_mirrors(): newvals = (mirrorurl.has_ipv4, mirrorurl.has_ipv6) if newvals != oldvals: logger.debug("values changed for %s", mirrorurl) - mirrorurl.save(force_update=True) + mirrorurl.save(update_fields=('has_ipv4', 'has_ipv6')) except socket.error, e: logger.warn("error resolving %s: %s", mirrorurl.hostname, e) diff --git a/packages/views/signoff.py b/packages/views/signoff.py index 17f3095c..c37aa0fc 100644 --- a/packages/views/signoff.py +++ b/packages/views/signoff.py @@ -45,7 +45,7 @@ def signoff_package(request, name, repo, arch, revoke=False): except Signoff.DoesNotExist: raise Http404 signoff.revoked = now() - signoff.save() + signoff.save(update_fields=('revoked',)) created = False else: # ensure we should even be accepting signoffs diff --git a/releng/management/commands/syncisos.py b/releng/management/commands/syncisos.py index 223c771b..c9f61964 100644 --- a/releng/management/commands/syncisos.py +++ b/releng/management/commands/syncisos.py @@ -53,7 +53,7 @@ def handle(self, *args, **options): if not existing.active: existing.active = True existing.removed = None - existing.save() + existing.save(update_fields=('active', 'removed')) # and then mark all other names as no longer active Iso.objects.filter(active=True).exclude(name__in=active_isos).update( active=False, removed=now()) diff --git a/todolists/views.py b/todolists/views.py index f333728a..9935987b 100644 --- a/todolists/views.py +++ b/todolists/views.py @@ -47,7 +47,7 @@ def flag(request, slug, pkg_id): else: tlpkg.status = TodolistPackage.INCOMPLETE tlpkg.user = request.user - tlpkg.save() + tlpkg.save(update_fields=('status', 'user', 'last_modified')) if request.is_ajax(): data = { 'status': tlpkg.get_status_display(), -- cgit v1.2.3-54-g00ecf From 5bc85244281efc916132c86046018d0ebe70b5e9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sun, 10 Feb 2013 12:45:24 -0600 Subject: Fix split packages sitemap We had a ton of duplicate entries included due to the query implicitly including a 'GROUP BY' clause on the default sorting by pkgname. Fix it and cut the sitemap down to the correct size without duplicate entries. Signed-off-by: Dan McGee --- packages/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils.py b/packages/utils.py index 49aeb8ce..ef6311eb 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -79,7 +79,7 @@ def get_split_packages_info(): pkgnames = Package.objects.values('pkgname') split_pkgs = Package.objects.exclude(pkgname=F('pkgbase')).exclude( pkgbase__in=pkgnames).values('pkgbase', 'repo', 'arch').annotate( - last_update=Max('last_update')) + last_update=Max('last_update')).order_by().distinct() all_arches = Arch.objects.in_bulk({s['arch'] for s in split_pkgs}) all_repos = Repo.objects.in_bulk({s['repo'] for s in split_pkgs}) for split in split_pkgs: -- cgit v1.2.3-54-g00ecf From 51a6669c3c8a44b6b9e7fd09f8253d3a36953eb6 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 16 Feb 2013 13:54:11 -0600 Subject: Defer loading of some JS on public index page We don't need typeahead and easter eggs working right away, so defer them into a onload event. Signed-off-by: Dan McGee --- templates/public/index.html | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/templates/public/index.html b/templates/public/index.html index 89aa3b8a..dad6a05b 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -212,10 +212,8 @@

    More Resources

    {% load cdn %}{% jquery %} - - {% endblock %} -- cgit v1.2.3-54-g00ecf From 1b323ed9b2ac170cea4207ae75fd2849be8ff646 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 16 Feb 2013 14:08:49 -0600 Subject: Fix missing template variable Signed-off-by: Dan McGee --- templates/releng/result_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/releng/result_list.html b/templates/releng/result_list.html index be5783b3..f6a825dc 100644 --- a/templates/releng/result_list.html +++ b/templates/releng/result_list.html @@ -6,7 +6,7 @@

    Results for: {% if option %}{{ option.verbose_name|title }}: {{ value }}{% endif %} - {{ iso_name|default:"" }} + {% if iso_name %}{{ iso_name|default:"" }}{% endif %}

    Go back to testing results

    -- cgit v1.2.3-54-g00ecf From d7f849afe4c141efda0ec6742031845fe2d03660 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 16 Feb 2013 14:09:01 -0600 Subject: Move all red/green/orange styles into single declaration We use these all over the place and can express them in a much shorter fashion. Signed-off-by: Dan McGee --- sitestatic/archweb.css | 52 +++++++++++++++----------------------------------- 1 file changed, 15 insertions(+), 37 deletions(-) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index dcc964ee..a0dfb2ec 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -1002,19 +1002,30 @@ ul.admin-actions { padding-left: 1.5em; } -/* todo lists (public and private) */ -.todo-table .complete { +/* colored yes/no type values */ +.todo-table .complete, +.signoff-yes, +#key-status .signed-yes, +#releng-result .success-yes, +#release-list .available-yes { color: green; } -.todo-table .incomplete { +.todo-table .incomplete, +.signoff-no, +#key-status .signed-no, +#releng-result .success-no, +#release-list .available-no { color: red; } -.todo-table .inprogress { +.todo-table .inprogress, +.signoff-bad { color: darkorange; } + +/* todo lists (public and private) */ .todo-info { margin: 0; color: #999; } @@ -1036,18 +1047,9 @@ ul.signoff-list { } .signoff-yes { - color: green; font-weight: bold; } -.signoff-no { - color: red; -} - -.signoff-bad { - color: darkorange; -} - .signoff-disabled { color: gray; } @@ -1071,30 +1073,6 @@ ul.signoff-list { position: relative; top: -0.9em; } -#releng-result .success-yes { - color: green; -} - -#releng-result .success-no { - color: red; -} - -#release-list .available-yes { - color: green; -} - -#release-list .available-no { - color: red; -} - -#key-status .signed-yes { - color: green; -} - -#key-status .signed-no { - color: red; -} - /* highlight current website in the navbar */ #archnavbar.anb-home ul li#anb-home a, #archnavbar.anb-packages ul li#anb-packages a, -- cgit v1.2.3-54-g00ecf From adc27750593aadb57f85f7b6817e4300f5ee180e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 16 Feb 2013 15:04:50 -0600 Subject: Paginator template cleanup --- templates/news/paginator.html | 10 +++++----- templates/packages/search_paginator.html | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/templates/news/paginator.html b/templates/news/paginator.html index fbd0546b..57fbeb15 100644 --- a/templates/news/paginator.html +++ b/templates/news/paginator.html @@ -1,20 +1,20 @@ {% if is_paginated %} +{% endblock %} + +{% block script_block %} {% load cdn %}{% jquery %}{% jquery_tablesorter %} diff --git a/templates/registration/login.html b/templates/registration/login.html index edbb77ea..82f74f58 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -21,7 +21,9 @@

    Developer Login

    +{% endblock %} +{% block script_block %} {% load cdn %}{% jquery %} -- cgit v1.2.3-54-g00ecf From 1364f689fd177e11ab04f0842035175baf80de35 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 20 Apr 2013 11:34:12 -0500 Subject: Minor CSS cleanups/tweaks Signed-off-by: Dan McGee --- sitestatic/archweb.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index 5c506914..b7d6e1ee 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -314,7 +314,7 @@ dl { dl dt, dl dd { margin-bottom: 4px; - padding: 8px 0px 4px; + padding: 8px 0 4px; font-weight: bold; border-top: 1px dotted #bbb; } @@ -420,7 +420,7 @@ table thead th.tablesorter-headerDesc { } table thead th.sorter-false { - background-image: url(); + background-image: none; cursor: default; } @@ -490,13 +490,13 @@ table thead th.sorter-false { h3 span.arrow { display: block; - width: 0px; - height: 0px; + width: 0; + height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid #1794D1; margin: 0 auto; - font-size: 0px; + font-size: 0; line-height: 0px; } -- cgit v1.2.3-54-g00ecf From 7873cb9a052c7991f3d91ddf4dc05caef6305cd7 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 20 Apr 2013 13:31:20 -0500 Subject: Unbreak the package differences page We had a weird conditional around everything on the page that doesn't really need to be there. Remove it. Signed-off-by: Dan McGee --- templates/packages/differences.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/templates/packages/differences.html b/templates/packages/differences.html index 3d3f6edf..b0b9b419 100644 --- a/templates/packages/differences.html +++ b/templates/packages/differences.html @@ -5,7 +5,6 @@ {% block navbarclass %}anb-packages{% endblock %} {% block content %} -{% if differences %}

    Package Differences by Architecture

    @@ -111,5 +110,4 @@

    Multilib Differences to Main Packages

    filter_packages(); }); -{% endif %} {% endblock %} -- cgit v1.2.3-54-g00ecf From bb18fa3323a0494a2774ea61911572b089d04b6d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 20 Apr 2013 13:32:09 -0500 Subject: Fix parsing issues when query string keys contain unicode This is dirty, but it works. There is probably a better and cleaner way to do all of this, but for now just fix it quickly. Signed-off-by: Dan McGee --- packages/templatetags/package_extras.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py index ef0e1aea..f7392a96 100644 --- a/packages/templatetags/package_extras.py +++ b/packages/templatetags/package_extras.py @@ -37,6 +37,12 @@ def __init__(self, sortfield): def render(self, context): qs = parse_qs(context['current_query']) + # This is really dirty. The crazy round trips we do on our query string + # mean we get things like u'\xe2\x98\x83' in our views, when we should + # have simply u'\u2603' or a byte string of the UTF-8 value. Force the + # keys and list of values to be byte strings only. + qs = {k.encode('latin-1'): [v.encode('latin-1') for v in vals] + for k, vals in qs.items()} if 'sort' in qs and self.sortfield in qs['sort']: if self.sortfield.startswith('-'): qs['sort'] = [self.sortfield[1:]] -- cgit v1.2.3-54-g00ecf From a22557811a24b68ef85d4271787c48d8d1e4fc99 Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Sun, 21 Apr 2013 02:49:16 -0400 Subject: catch a few more instances of archlinux --- README.BRANDING | 7 +++++++ devel/forms.py | 3 ++- devel/utils.py | 4 ++-- feeds.py | 2 +- settings.py | 2 ++ templates/news/list.html | 2 +- templates/packages/removed.html | 2 +- templates/packages/search.html | 2 +- templates/public/developer_list.html | 2 +- templates/public/download.html | 2 +- templates/releng/release_detail.html | 2 +- templates/releng/release_list.html | 4 ++-- 12 files changed, 22 insertions(+), 12 deletions(-) diff --git a/README.BRANDING b/README.BRANDING index 00d2d1b0..e438ccc1 100644 --- a/README.BRANDING +++ b/README.BRANDING @@ -25,6 +25,9 @@ Files with minor Arch stuff that's just easier to patch `templates/packages/flag.html` * link to "arch-general" mailing list +`templates/packages/removed.html` + * link to AUR + `templates/packages/search.html` * link to AUR @@ -32,6 +35,10 @@ Files with minor Arch stuff that's just easier to patch * links to AUR * links to feeds on `bugs.archlinux.org` +`urls.py` +`releng/views.py` +`releng/models.py` + Files with a significant amount of Arch-specific content: --------------------------------------------------------- diff --git a/devel/forms.py b/devel/forms.py index 861a576c..7f7c281e 100644 --- a/devel/forms.py +++ b/devel/forms.py @@ -2,6 +2,7 @@ from string import ascii_letters, digits from django import forms +from django.conf import settings from django.contrib.auth.models import User, Group from django.contrib.sites.models import Site from django.core.mail import send_mail @@ -92,7 +93,7 @@ def save(self, commit=True): send_mail("Your new archweb account", template.render(ctx), - 'Arch Website Notification ', + settings.BRANDING_EMAIL, [user.email], fail_silently=False) diff --git a/devel/utils.py b/devel/utils.py index 340841f5..7dd64972 100644 --- a/devel/utils.py +++ b/devel/utils.py @@ -1,5 +1,6 @@ import re +from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db import connection @@ -74,9 +75,8 @@ def username_email(name, email): if email and '@' in email: # split email addr at '@' symbol, ensure domain matches # or is a subdomain of archlinux.org - # TODO: configurable domain/regex somewhere? username, domain = email.split('@', 1) - if re.match(r'^(.+\.)?archlinux.org$', domain): + if re.match(settings.DOMAIN_RE, domain): return User.objects.get(username=username) return None diff --git a/feeds.py b/feeds.py index 9c103b8c..e96c2ca4 100644 --- a/feeds.py +++ b/feeds.py @@ -160,7 +160,7 @@ def item_author_name(self, item): class ReleaseFeed(Feed): feed_type = GuidNotPermalinkFeed - title = 'Arch Linux: Releases' + title = settings.BRANDING_DISTRONAME+': Releases' link = '/download/' description = 'Release ISOs' subtitle = description diff --git a/settings.py b/settings.py index 49ef1898..1cb85fc1 100644 --- a/settings.py +++ b/settings.py @@ -177,6 +177,8 @@ 'http://tracker.archlinux.org:6969/announce', ) +DOMAIN_RE = r'^(.+\.)?archlinux.org$' + BRANDING_APPNAME = 'archweb' BRANDING_DISTRONAME = 'Arch Linux' BRANDING_SHORTNAME = 'Arch' diff --git a/templates/news/list.html b/templates/news/list.html index 0ddf3b5e..3cd460ae 100644 --- a/templates/news/list.html +++ b/templates/news/list.html @@ -2,7 +2,7 @@ {% block title %}{{ BRANDING_DISTRONAME }} - News{% endblock %} {% block head %} - + {% endblock %} {% block content %} diff --git a/templates/packages/removed.html b/templates/packages/removed.html index f188b6db..2d730130 100644 --- a/templates/packages/removed.html +++ b/templates/packages/removed.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% load package_extras %} -{% block title %}Arch Linux - Not Available - {{ name }} {{ version }} ({{ arch.name }}){% endblock %} +{% block title %}{{ BRANDING_DISTRONAME }} - Not Available - {{ name }} {{ version }} ({{ arch.name }}){% endblock %} {% block navbarclass %}anb-packages{% endblock %} {% block content %} diff --git a/templates/packages/search.html b/templates/packages/search.html index b5b08268..e1be4256 100644 --- a/templates/packages/search.html +++ b/templates/packages/search.html @@ -6,7 +6,7 @@ {% block head %} {% if is_paginated and page_obj.number > 1 %}{% endif %} - + {% endblock %} {% block content %} diff --git a/templates/public/developer_list.html b/templates/public/developer_list.html index 4401d97b..15a6c8bb 100644 --- a/templates/public/developer_list.html +++ b/templates/public/developer_list.html @@ -23,7 +23,7 @@
    - +

    {{ dev.get_full_name }}{% if prof.latin_name %} ({{ prof.latin_name}}){% endif %}

    diff --git a/templates/public/download.html b/templates/public/download.html index 01e46f66..274d6cfd 100644 --- a/templates/public/download.html +++ b/templates/public/download.html @@ -29,7 +29,7 @@

    Release Info

    {% if release.version %}
  • Current Release: {{ release.version }}
  • {% endif %} {% if release.kernel_version %}
  • Included Kernel: {{ release.kernel_version }}
  • {% endif %} {% if release.file_size %}
  • ISO Size: {{ release.file_size|filesizeformat }}
  • {% endif %} -
  • Installation Guide
  • +
  • Installation Guide
  • Resources: