Permalink
Browse files

Merge pull request #28 from brandonsavage/bug863381

Fixes 863381 - Creates a system for internal redirects of products inside Bouncer.
  • Loading branch information...
2 parents 2ae1122 + c7bc42b commit 226bcff66ef0069c58d6091e877c048033d498c9 @brandonsavage brandonsavage committed Aug 27, 2013
View
33 apps/api/templates/api/docs/create_update_alias.html
@@ -0,0 +1,33 @@
+{% extends "api/api_doc_template.html" %}
+
+{% block description %}
+Create or update a product alias
+{% endblock %}
+
+{% block auth %}Needs HTTP authentication. Staff members only.{% endblock %}
+
+{% block post_required %}
+* ``alias``: The alias that you wish to create or update.
+* ``related_product``: The product name you wish to have the alias redirect
+{% endblock %}
+
+{% block errorcodes %}
+* ``101``: Unknown validation error occurred
+* ``102``: Alias name not provided
+* ``103``: Related product name not provided
+* ``104``: The product name specified was not a valid product name
+* ``105``: The alias name specified matches the name of an existing product and must be different
+{% endblock %}
+
+{% block example %}
+Success:
+
+ <success>Created/updated alias firefox-latest</success>
+
+Failure:
+
+ <error number="101">
+ alias name is required.
+ </error>
+{% endblock %}
+
View
63 apps/api/tests/test_products.py
@@ -4,6 +4,7 @@
from django.core.urlresolvers import reverse
from mirror.models import Product
+from mirror.forms import ProductAliasForm
from . import testcases
@@ -170,3 +171,65 @@ def test_product_language_delete_all(self):
assert not myprod.languages.count(), (
'Wildcard must delete all languages.')
+
+ def test_create_update_alias(self):
+
+ Product.objects.create(name='MyTestProduct')
+
+ response = self.c.post(reverse('api.views.create_update_alias'),
+ {'alias': 'my-test-alias',
+ 'related_product': 'MyTestProduct'})
+
+ self.assertEqual(response.status_code, 200,
+ 'Status code should be 200 when alias created')
+
+ def test_create_update_alias_requires_alias_name(self):
+ Product.objects.create(name='MyTestProduct')
+
+ response = self.c.post(reverse('api.views.create_update_alias'),
+ {'alias': '',
+ 'related_product': 'MyTestProduct'})
+
+ xmldoc = minidom.parseString(response.content)
+
+ msg = xmldoc.getElementsByTagName('error')
+ errno = msg[0].getAttribute('number')
+ self.assertEqual(int(errno), ProductAliasForm.E_ALIAS_REQUIRED,
+ 'alias is a required field')
+
+ def test_create_update_alias_requires_valid_product_name(self):
+ response = self.c.post(reverse('api.views.create_update_alias'),
+ {'alias': 'my-test-alias',
+ 'related_product': ''})
+
+ xmldoc = minidom.parseString(response.content)
+
+ msg = xmldoc.getElementsByTagName('error')
+ errno = msg[0].getAttribute('number')
+
+ self.assertEqual(int(errno), ProductAliasForm.E_RELATED_NAME_REQUIRED,
+ 'related_product is a required field')
+
+ response = self.c.post(reverse('api.views.create_update_alias'),
+ {'alias': 'my-test-alias',
+ 'related_product': 'MyTestProduct'})
+
+ xmldoc = minidom.parseString(response.content)
+
+ msg = xmldoc.getElementsByTagName('error')
+ errno = msg[0].getAttribute('number')
+
+ self.assertEqual(int(errno), ProductAliasForm.E_PRODUCT_DOESNT_EXIST,
+ 'Must provide a valid product name')
+
+ Product.objects.create(name='MyTestProduct')
+
+ response = self.c.post(reverse('api.views.create_update_alias'),
+ {'alias': 'myMyTestProduct',
+ 'related_product': 'MyTestProduct'})
+
+ xmldoc = minidom.parseString(response.content)
+ msg = xmldoc.getElementsByTagName('error')
+ errno = msg[0].getAttribute('number')
+ self.assertEqual(int(errno), ProductAliasForm.E_ALIAS_PRODUCT_MATCH,
+ 'Cannot specify the same alias as a product name')
View
1 apps/api/tests/testcases.py
@@ -16,6 +16,7 @@ def setUp(self):
self.user = User.objects.create_user(
username, 'lennon@thebeatles.com', pw
)
+
self.user.is_staff = True
self.user.save()
self.c = test.client.Client()
View
2 apps/api/urls.py
@@ -16,5 +16,7 @@
(r'^uptake/?$', 'uptake'),
(r'^mirror_list/?$', 'mirror_list'),
+
+ (r'^create_update_alias/?$', 'create_update_alias'),
)
View
64 apps/api/views.py
@@ -10,12 +10,11 @@
from product_details import product_details
from api.decorators import has_perm_or_basicauth, logged_in_or_basicauth
-from mirror.models import Location, Mirror, OS, Product
-
+from mirror.models import Location, Mirror, OS, Product, ProductAlias
+from mirror.forms import ProductAliasForm
HTTP_AUTH_REALM = 'Bouncer API'
-
def _get_command_list():
templates = os.listdir(os.path.join(os.path.dirname(__file__), 'templates',
'api', 'docs'))
@@ -357,6 +356,65 @@ def location_delete(request):
return xml.success('Deleted location: %s' % location)
+@require_POST
+@csrf_exempt
+@has_perm_or_basicauth("mirror.create_update_alias", HTTP_AUTH_REALM)
+def create_update_alias(request):
+ """Create or update an alias for a product"""
+
+ xml = XMLRenderer()
+
+ form = ProductAliasForm(request.POST)
+ if not form.is_valid():
+ if 'alias' in form.errors:
+ if 'required' in form.errors['alias'][0]:
+ return xml.error(
+ 'alias name is required.',
+ errno=form.E_ALIAS_REQUIRED
+ )
+
+ if 'same name' in form.errors['alias'][0]:
+ return xml.error(
+ 'You cannot create an alias with the same name as a product',
+ errno=form.E_ALIAS_PRODUCT_MATCH
+ )
+ if 'related_product' in form.errors:
+ if 'required' in form.errors['related_product'][0]:
+ return xml.error(
+ 'related_product name is required.',
+ errno=form.E_RELATED_NAME_REQUIRED
+ )
+ if 'same name as an existing' in form.errors['related_product'][0]:
+ return xml.error(
+ 'You cannot create alias with the same name as a product',
+ errno=form.E_ALIAS_PRODUCT_MATCH
+ )
+ if 'invalid' in form.errors['related_product'][0]:
+ return xml.error(
+ 'You must specify a valid product to match with an alias',
+ errno=form.E_PRODUCT_DOESNT_EXIST
+ )
+
+ return xml.error(
+ 'There was a problem validating the data provided',
+ errno=form.E_ALIAS_GENERAL_VALIDATION_ERROR
+ )
+
+ alias = form.cleaned_data['alias']
+ redirect = form.cleaned_data['related_product']
+
+ alias_obj, created = ProductAlias.objects.get_or_create(
+ alias=alias,
+ defaults={'related_product': redirect}
+ )
+
+ if not created:
+ alias_obj.related_product = redirect
+ alias_obj.save()
+
+ return xml.success('Created/updated alias %s' % alias)
+
+
class XMLRenderer(object):
"""Render API data as XML"""
View
13 apps/mirror/admin.py
@@ -1,6 +1,14 @@
from django.contrib import admin
-from mirror.models import Mirror, OS, Product, Location, ProductLanguage
+from mirror.models import (Mirror, OS, Product, Location, ProductLanguage,
+ ProductAlias)
+from mirror.forms import ProductAliasForm
+
+
+class ProductAliasAdmin(admin.ModelAdmin):
+ list_display = ('alias', 'related_product')
+ form = ProductAliasForm
+admin.site.register(ProductAlias, ProductAliasAdmin)
class LocationAdmin(admin.ModelAdmin):
@@ -34,7 +42,8 @@ class ProductLanguageInline(admin.TabularInline):
class ProductAdmin(admin.ModelAdmin):
- list_display = ('name', 'priority', 'count', 'active', 'checknow', 'ssl_only')
+ list_display = ('name', 'priority', 'count', 'active', 'checknow',
+ 'ssl_only')
list_filter = ('active', 'checknow', 'ssl_only')
ordering = ('name',)
actions = ('mark_for_checknow', 'unmark_for_checknow')
View
29 apps/mirror/forms.py
@@ -1,8 +1,35 @@
from django import forms
-from models import Product
+from models import Product, ProductAlias
+from django.core import validators
class UptakeForm(forms.Form):
p = forms.ModelMultipleChoiceField(
queryset=Product.objects.order_by('name'),
label='Products')
+
+class ProductAliasForm(forms.ModelForm):
+
+ E_ALIAS_GENERAL_VALIDATION_ERROR= 101
+ E_ALIAS_REQUIRED = 102
+ E_PRODUCT_DOESNT_EXIST = 103
+ E_ALIAS_PRODUCT_MATCH = 104
+ E_RELATED_NAME_REQUIRED = 105
+
+ class Meta:
+ model = ProductAlias
+
+ def clean_alias(self):
+ if Product.objects.filter(name=self.cleaned_data['alias']).exists():
+ raise forms.ValidationError(
+ "Your alias cannot share the same name as an existing product!"
+ )
+ return self.cleaned_data['alias']
+
+ def clean_related_product(self):
+ if not Product.objects.filter(
+ name=self.cleaned_data['related_product']).exists():
+ raise forms.ValidationError(
+ "The product you entered was invalid"
+ )
+ return self.cleaned_data['related_product']
View
149 ...migrations/0004_auto__add_productalias__add_field_locationmirrormap_healthy__add_field.py
@@ -0,0 +1,149 @@
+# -*- 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 model 'ProductAlias'
+ db.create_table('mirror_aliases', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('alias', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
+ ('related_product', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ))
+ db.send_create_signal('mirror', ['ProductAlias'])
+
+ # Adding field 'LocationMirrorMap.healthy'
+ db.add_column('mirror_location_mirror_map', 'healthy',
+ self.gf('django.db.models.fields.BooleanField')(default=False),
+ keep_default=False)
+
+ # Adding field 'Product.ssl_only'
+ db.add_column('mirror_products', 'ssl_only',
+ self.gf('django.db.models.fields.BooleanField')(default=False),
+ keep_default=False)
+
+
+ def backwards(self, orm):
+ # Deleting model 'ProductAlias'
+ db.delete_table('mirror_aliases')
+
+ # Deleting field 'LocationMirrorMap.healthy'
+ db.delete_column('mirror_location_mirror_map', 'healthy')
+
+ # Deleting field 'Product.ssl_only'
+ db.delete_column('mirror_products', 'ssl_only')
+
+
+ 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'})
+ },
+ 'geoip.region': {
+ 'Meta': {'object_name': 'Region', 'db_table': "'geoip_regions'"},
+ 'fallback': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['geoip.Region']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'prevent_global_fallback': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'priority': ('django.db.models.fields.IntegerField', [], {}),
+ 'throttle': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'mirror.location': {
+ 'Meta': {'unique_together': "(('product', 'os'),)", 'object_name': 'Location', 'db_table': "'mirror_locations'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'os': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['mirror.OS']"}),
+ 'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['mirror.Product']"})
+ },
+ 'mirror.locationmirrorlanguageexception': {
+ 'Meta': {'unique_together': "(('lmm', 'lang'),)", 'object_name': 'LocationMirrorLanguageException', 'db_table': "'mirror_lmm_lang_exceptions'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'lang': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_column': "'language'"}),
+ 'lmm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'lang_exceptions'", 'db_column': "'location_mirror_map_id'", 'to': "orm['mirror.LocationMirrorMap']"})
+ },
+ 'mirror.locationmirrormap': {
+ 'Meta': {'object_name': 'LocationMirrorMap', 'db_table': "'mirror_location_mirror_map'"},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'healthy': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'location': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['mirror.Location']"}),
+ 'mirror': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['mirror.Mirror']"})
+ },
+ 'mirror.mirror': {
+ 'Meta': {'object_name': 'Mirror', 'db_table': "'mirror_mirrors'"},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'baseurl': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'contacts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'count': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '20', 'decimal_places': '0', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}),
+ 'rating': ('django.db.models.fields.IntegerField', [], {}),
+ 'regions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['geoip.Region']", 'db_table': "'geoip_mirror_region_map'", 'symmetrical': 'False'})
+ },
+ 'mirror.os': {
+ 'Meta': {'object_name': 'OS'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+ 'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+ },
+ 'mirror.product': {
+ 'Meta': {'object_name': 'Product', 'db_table': "'mirror_products'"},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'checknow': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'count': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '20', 'decimal_places': '0'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'ssl_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'mirror.productalias': {
+ 'Meta': {'object_name': 'ProductAlias', 'db_table': "'mirror_aliases'"},
+ 'alias': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'related_product': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'mirror.productlanguage': {
+ 'Meta': {'unique_together': "(('product', 'lang'),)", 'object_name': 'ProductLanguage', 'db_table': "'mirror_product_langs'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'lang': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_column': "'language'"}),
+ 'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'languages'", 'to': "orm['mirror.Product']"})
+ }
+ }
+
+ complete_apps = ['mirror']
View
10 apps/mirror/models.py
@@ -88,6 +88,16 @@ class Meta:
db_table = 'mirror_products'
+class ProductAlias(models.Model):
+ """An alias that will redirect a user to a valid product
+ (e.g. firefox-latest)"""
+ alias = models.SlugField(max_length=255, unique=True)
+ related_product = models.CharField(max_length=255, unique=False)
+
+ class Meta:
+ db_table = "mirror_aliases"
+
+
class ProductLanguage(models.Model):
"""
The languages a product is available in. No entries in this table means
View
31 bouncer/php/index.php
@@ -24,27 +24,6 @@
$where_lang = $_GET['lang'];
else
$where_lang = 'en-US';
-
- // Special case for bug 398366
- if ($product_name == 'firefox-latest') {
- $string = file_get_contents(INC . '/product-details/json/firefox_versions.json');
- $firefox_versions = json_decode($string);
- $latest = $firefox_versions->{'LATEST_FIREFOX_VERSION'};
-
- $redirect_url = WEBPATH . '?product=firefox-' . $latest .
- '&os=' . $os_name . '&lang=' . $where_lang;
- // if we are just testing, then just print and exit.
- if (!empty($_GET['print'])) {
- show_no_cache_headers();
- print(htmlentities('Location: ' . $redirect_url . '&print=1'));
- exit;
- }
-
- // otherwise, by default, redirect them and exit
- show_no_cache_headers();
- header('Location: ' . $redirect_url);
- exit;
- }
require_once(LIB.'/sdo2.php');
require_once(LIB.'/memcaching.php');
@@ -58,6 +37,16 @@
);
$sdo = new SDO2($mc, $dbwrite);
+
+ // New alias code for bug 863381
+ $alias_sql = 'SELECT * FROM mirror_aliases WHERE alias = ?';
+ $alias = $sdo->get_one($alias_sql, array($product_name), SDO2::FETCH_ASSOC);
+ if ($alias) {
+ // We have a product name, we'll swap it in for the $product_name variable
+ // and move on.
+ $product_name = $alias['related_product'];
+ }
+
// get OS ID
$os_id = name_to_id($sdo, 'mirror_os','id','name',$os_name);
View
8 sql/incremental.sql
@@ -124,3 +124,11 @@ ALTER TABLE mirror_location_mirror_map ADD COLUMN healthy tinyint(4) NOT NULL DE
-- more space for sentry logs (bug 777516)
ALTER TABLE `sentry_log` MODIFY `reason` MEDIUMTEXT;
+
+-- create table for aliases (bug 863381)
+CREATE TABLE `mirror_aliases` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `alias` varchar(255) NOT NULL UNIQUE,
+ `related_product` varchar(255) NOT NULL
+);
+ALTER TABLE mirror_aliases ADD CONSTRAINT uniq_alias UNIQUE (alias);

0 comments on commit 226bcff

Please sign in to comment.