diff --git a/moztrap/model/__init__.py b/moztrap/model/__init__.py index 2aabaee6..e1b9c564 100644 --- a/moztrap/model/__init__.py +++ b/moztrap/model/__init__.py @@ -7,7 +7,7 @@ from registration.models import RegistrationProfile from .mtmodel import ConcurrencyError -from .core.models import Product, ProductVersion +from .core.models import Product, ProductVersion, ApiKey from .core.auth import User, Role, Permission from .environments.models import Environment, Profile, Element, Category from .execution.models import Run, RunSuite, RunCaseVersion, Result, StepResult diff --git a/moztrap/model/core/admin.py b/moztrap/model/core/admin.py index 3a6f0043..8facb058 100644 --- a/moztrap/model/core/admin.py +++ b/moztrap/model/core/admin.py @@ -2,8 +2,8 @@ from preferences.admin import PreferencesAdmin -from ..mtadmin import MTTabularInline, TeamModelAdmin -from .models import Product, ProductVersion, CorePreferences +from ..mtadmin import MTTabularInline, MTModelAdmin, TeamModelAdmin +from .models import Product, ProductVersion, CorePreferences, ApiKey @@ -12,7 +12,13 @@ class ProductVersionInline(MTTabularInline): extra = 0 +class ApiKeyAdmin(MTModelAdmin): + list_display = ["owner", "active", "key"] + list_filter = ["active"] + + admin.site.register(Product, TeamModelAdmin, inlines=[ProductVersionInline]) admin.site.register(ProductVersion, TeamModelAdmin, list_filter=["product"]) admin.site.register(CorePreferences, PreferencesAdmin) +admin.site.register(ApiKey, ApiKeyAdmin) diff --git a/moztrap/model/core/migrations/0006_auto__add_apikey.py b/moztrap/model/core/migrations/0006_auto__add_apikey.py new file mode 100644 index 00000000..0e4a2363 --- /dev/null +++ b/moztrap/model/core/migrations/0006_auto__add_apikey.py @@ -0,0 +1,168 @@ +# -*- 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 'ApiKey' + db.create_table('core_apikey', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('created_on', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2012, 5, 9, 0, 0))), + ('created_by', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='+', null=True, to=orm['auth.User'])), + ('modified_on', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2012, 5, 9, 0, 0))), + ('modified_by', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='+', null=True, to=orm['auth.User'])), + ('deleted_on', self.gf('django.db.models.fields.DateTimeField')(db_index=True, null=True, blank=True)), + ('deleted_by', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='+', null=True, to=orm['auth.User'])), + ('cc_version', self.gf('django.db.models.fields.IntegerField')(default=0)), + ('owner', self.gf('django.db.models.fields.related.ForeignKey')(related_name='api_keys', to=orm['auth.User'])), + ('key', self.gf('django.db.models.fields.CharField')(unique=True, max_length=36)), + ('active', self.gf('django.db.models.fields.BooleanField')(default=True, db_index=True)), + )) + db.send_create_signal('core', ['ApiKey']) + + def backwards(self, orm): + # Deleting model 'ApiKey' + db.delete_table('core_apikey') + + 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', [], {'unique': 'True', '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'}) + }, + 'core.apikey': { + 'Meta': {'object_name': 'ApiKey'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'cc_version': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'deleted_on': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'modified_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'api_keys'", 'to': "orm['auth.User']"}) + }, + 'core.product': { + 'Meta': {'ordering': "['name']", 'object_name': 'Product'}, + 'cc_version': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'deleted_on': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'has_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'modified_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'own_team': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'core.productversion': { + 'Meta': {'ordering': "['product', 'order']", 'object_name': 'ProductVersion'}, + 'cc_version': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'deleted_on': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'environments': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'productversion'", 'symmetrical': 'False', 'to': "orm['environments.Environment']"}), + 'has_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'latest': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'modified_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'own_team': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False', 'blank': 'True'}), + 'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'versions'", 'to': "orm['core.Product']"}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'environments.category': { + 'Meta': {'ordering': "['name']", 'object_name': 'Category'}, + 'cc_version': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'deleted_on': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'modified_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'environments.element': { + 'Meta': {'ordering': "['name']", 'object_name': 'Element'}, + 'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'elements'", 'to': "orm['environments.Category']"}), + 'cc_version': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'deleted_on': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'modified_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'environments.environment': { + 'Meta': {'object_name': 'Environment'}, + 'cc_version': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'deleted_on': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'elements': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'environments'", 'symmetrical': 'False', 'to': "orm['environments.Element']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'modified_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'profile': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'environments'", 'null': 'True', 'to': "orm['environments.Profile']"}) + }, + 'environments.profile': { + 'Meta': {'object_name': 'Profile'}, + 'cc_version': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'deleted_on': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}), + 'modified_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 9, 0, 0)'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + } + } + + complete_apps = ['core'] \ No newline at end of file diff --git a/moztrap/model/core/models.py b/moztrap/model/core/models.py index 1b7ec707..1f020e14 100644 --- a/moztrap/model/core/models.py +++ b/moztrap/model/core/models.py @@ -2,6 +2,8 @@ Core MozTrap models (Product). """ +import uuid + from django.core.exceptions import ValidationError from django.db import models @@ -9,8 +11,8 @@ from preferences.models import Preferences from ..environments.models import HasEnvironmentsModel -from ..mtmodel import MTModel, TeamModel -from .auth import Role +from ..mtmodel import MTModel, MTManager, TeamModel +from .auth import Role, User @@ -188,3 +190,39 @@ class CorePreferences(Preferences): class Meta: verbose_name_plural = "core preferences" + + + +class ApiKeyManager(MTManager): + use_for_related_fields = True + + def active(self): + return self.get_query_set().filter(active=True) + + + +class ApiKey(MTModel): + owner = models.ForeignKey(User, related_name="api_keys") + key = models.CharField(max_length=36, unique=True) + active = models.BooleanField(default=True, db_index=True) + + objects = ApiKeyManager() + + + def __unicode__(self): + return self.key + + + @classmethod + def generate(cls, owner, user=None): + """ + Generate, save and return a new API key. + + ``owner`` is the owner of the new key, ``user`` is the creating user. + + """ + if user is None: + user = owner + + return cls.objects.create( + owner=owner, user=user, key=unicode(uuid.uuid4())) diff --git a/moztrap/view/users/urls.py b/moztrap/view/users/urls.py index a08496c0..0e67282d 100644 --- a/moztrap/view/users/urls.py +++ b/moztrap/view/users/urls.py @@ -20,6 +20,7 @@ "password_reset_confirm", name="auth_password_reset_confirm"), url(r"^set_name/$", "set_username", name="auth_set_username"), + url(r"^(?P\d+)/apikey/$", "create_apikey", name="auth_create_apikey"), # registration ----------------------------------------------------------- diff --git a/moztrap/view/users/views.py b/moztrap/view/users/views.py index 3cd6114c..b81a6a55 100644 --- a/moztrap/view/users/views.py +++ b/moztrap/view/users/views.py @@ -6,7 +6,7 @@ from django.conf import settings from django.core.urlresolvers import reverse -from django.shortcuts import redirect, render +from django.shortcuts import redirect, render, get_object_or_404 from django.views.decorators.http import require_POST from django.contrib.auth import REDIRECT_FIELD_NAME, views as auth_views @@ -18,6 +18,7 @@ from registration import views as registration_views from session_csrf import anonymous_csrf +from moztrap import model from . import forms @@ -168,3 +169,14 @@ def set_username(request): return render( request, "users/set_username_form.html", {"form": form, "next": next}) + + + +@require_POST +@login_required +def create_apikey(request, user_id): + """Generate an API key for the given user; redirect to their edit page.""" + user = get_object_or_404(model.User, pk=user_id) + model.ApiKey.generate(owner=user, user=request.user) + + return redirect("manage_user_edit", user_id=user_id) diff --git a/sass/sections/manage/forms/_user-forms.sass b/sass/sections/manage/forms/_user-forms.sass index 7df3b019..b84819fd 100644 --- a/sass/sections/manage/forms/_user-forms.sass +++ b/sass/sections/manage/forms/_user-forms.sass @@ -17,4 +17,17 @@ +columns(6,18) .value, .errorlist +columns(12,18) - +omega(18) \ No newline at end of file + +omega(18) + +.apikeys + +pie-clearfix + +trailer + +pad(6,6,24) + h3 + +demi + +adjust-leading-to(2) + display: inline-block + .apikey-form + display: inline-block + button + +button-style($lighter,'ui/refresh.png') diff --git a/static/css/screen.css b/static/css/screen.css index c82b9d46..83cfe736 100644 --- a/static/css/screen.css +++ b/static/css/screen.css @@ -4000,6 +4000,70 @@ h2 + .select-env-link:active, .selectenvhead + .select-env-link:active { #margin-left: -1em; } +.apikeys { + *zoom: 1; + margin-bottom: 1.5em; + padding-left: 25.263%; + padding-right: 25.263%; +} +.apikeys:after { + content: ""; + display: table; + clear: both; +} +.apikeys h3 { + font-weight: 500; + line-height: 3em; + display: inline-block; +} +.apikeys .apikey-form { + display: inline-block; +} +.apikeys button { + font-weight: 500; + -webkit-border-radius: 0.296em; + -moz-border-radius: 0.296em; + -ms-border-radius: 0.296em; + -o-border-radius: 0.296em; + border-radius: 0.296em; + border-style: solid; + border-width: 0.071em; + padding: 0.304em; + text-shadow: rgba(255, 255, 255, 0.444) 1px 1px 0; + color: #454545; + border-color: #666666; + background-color: #fafafa; + background-repeat: no-repeat; + padding-left: 1em; + padding-right: 1em; + background-repeat: no-repeat; + padding-left: 24px; + background-position: 9px, left; + background-image: url('../images/ui/refresh.png?1333395141'), -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(255, 255, 255, 0.296)), color-stop(50%, rgba(255, 255, 255, 0)), color-stop(50%, rgba(69, 69, 69, 0)), color-stop(100%, rgba(69, 69, 69, 0.296))); + background-image: url('../images/ui/refresh.png?1333395141'), -webkit-linear-gradient(top center, rgba(255, 255, 255, 0.296) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.296)); + background-image: url('../images/ui/refresh.png?1333395141'), -moz-linear-gradient(top center, rgba(255, 255, 255, 0.296) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.296)); + background-image: url('../images/ui/refresh.png?1333395141'), -o-linear-gradient(top center, rgba(255, 255, 255, 0.296) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.296)); + background-image: url('../images/ui/refresh.png?1333395141'), -ms-linear-gradient(top center, rgba(255, 255, 255, 0.296) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.296)); + background-image: url('../images/ui/refresh.png?1333395141'), linear-gradient(top center, rgba(255, 255, 255, 0.296) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.296)); + cursor: pointer; +} +.apikeys button:hover, .apikeys button:focus { + background-image: url('../images/ui/refresh.png?1333395141'), -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(255, 255, 255, 0.667)), color-stop(50%, rgba(255, 255, 255, 0)), color-stop(50%, rgba(69, 69, 69, 0)), color-stop(100%, rgba(69, 69, 69, 0.444))); + background-image: url('../images/ui/refresh.png?1333395141'), -webkit-linear-gradient(top center, rgba(255, 255, 255, 0.667) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.444)); + background-image: url('../images/ui/refresh.png?1333395141'), -moz-linear-gradient(top center, rgba(255, 255, 255, 0.667) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.444)); + background-image: url('../images/ui/refresh.png?1333395141'), -o-linear-gradient(top center, rgba(255, 255, 255, 0.667) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.444)); + background-image: url('../images/ui/refresh.png?1333395141'), -ms-linear-gradient(top center, rgba(255, 255, 255, 0.667) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.444)); + background-image: url('../images/ui/refresh.png?1333395141'), linear-gradient(top center, rgba(255, 255, 255, 0.667) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.444)); +} +.apikeys button:active { + background-image: url('../images/ui/refresh.png?1333395141'), -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, rgba(255, 255, 255, 0.296)), color-stop(50%, rgba(255, 255, 255, 0)), color-stop(50%, rgba(69, 69, 69, 0)), color-stop(100%, rgba(69, 69, 69, 0.296))); + background-image: url('../images/ui/refresh.png?1333395141'), -webkit-linear-gradient(bottom center, rgba(255, 255, 255, 0.296) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.296)); + background-image: url('../images/ui/refresh.png?1333395141'), -moz-linear-gradient(bottom center, rgba(255, 255, 255, 0.296) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.296)); + background-image: url('../images/ui/refresh.png?1333395141'), -o-linear-gradient(bottom center, rgba(255, 255, 255, 0.296) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.296)); + background-image: url('../images/ui/refresh.png?1333395141'), -ms-linear-gradient(bottom center, rgba(255, 255, 255, 0.296) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.296)); + background-image: url('../images/ui/refresh.png?1333395141'), linear-gradient(bottom center, rgba(255, 255, 255, 0.296) 0%, rgba(255, 255, 255, 0) 50%, rgba(69, 69, 69, 0) 50%, rgba(69, 69, 69, 0.296)); +} + .tag-form { padding-right: 25.263%; } diff --git a/templates/manage/user/edit_user.html b/templates/manage/user/edit_user.html index c8ef8929..513ce788 100644 --- a/templates/manage/user/edit_user.html +++ b/templates/manage/user/edit_user.html @@ -3,3 +3,25 @@ {% block sectionid %}edit-user{% endblock %} {% block formid %}user-edit-form{% endblock %} {% block formtitle %}Edit User '{{ subject }}'{% endblock %} + +{% block afterform %} +
+ {% with subject.api_keys.active as apikeys %} + + {% if apikeys %} +

API Keys

+ + {% else %} +
+ {% csrf_token %} + +
+ {% endif %} + + {% endwith %} +
+{% endblock %} diff --git a/templates/manage/user/user_form.html b/templates/manage/user/user_form.html index 0b1749a5..7d77ed8a 100644 --- a/templates/manage/user/user_form.html +++ b/templates/manage/user/user_form.html @@ -19,5 +19,8 @@

{% block formtitle %}{% endblock %}

+ + {% block afterform %}{% endblock %} + {% endblock content %} diff --git a/tests/factories.py b/tests/factories.py index 73856052..a55f626c 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -106,6 +106,14 @@ class RoleFactory(factory.Factory): +class ApiKeyFactory(factory.Factory): + FACTORY_FOR = model.ApiKey + + owner = factory.SubFactory(UserFactory) + key = factory.Sequence(lambda n: "Test-ApiKey-{0}".format(n)) + + + class ProductFactory(TeamFactoryMixin, factory.Factory): FACTORY_FOR = model.Product diff --git a/tests/model/core/admin/test_apikey.py b/tests/model/core/admin/test_apikey.py new file mode 100644 index 00000000..cc2c217b --- /dev/null +++ b/tests/model/core/admin/test_apikey.py @@ -0,0 +1,25 @@ +""" +Tests for ApiKey admin. + +""" +from tests import case + + + +class ApiKeyAdminTest(case.admin.AdminTestCase): + app_label = "core" + model_name = "apikey" + + + def test_changelist(self): + """ApiKey changelist page loads without error, contains key.""" + self.F.ApiKeyFactory.create(key="Test API Key") + + self.get(self.changelist_url).mustcontain("Test API Key") + + + def test_change_page(self): + """ApiKey change page loads without error, contains key.""" + k = self.F.ApiKeyFactory.create(key="Test API Key") + + self.get(self.change_url(k)).mustcontain("Test API Key") diff --git a/tests/model/core/models/test_apikey.py b/tests/model/core/models/test_apikey.py new file mode 100644 index 00000000..e13e1a15 --- /dev/null +++ b/tests/model/core/models/test_apikey.py @@ -0,0 +1,50 @@ +""" +Tests for ApiKey model. + +""" +from mock import patch + +from tests import case + + + +class ApiKeyTest(case.DBTestCase): + """Tests for ApiKey model.""" + def test_unicode(self): + """Unicode representation is the key.""" + k = self.F.ApiKeyFactory.build(key="12345") + + self.assertEqual(unicode(k), u"12345") + + + def test_active(self): + """Manager has method to return active keys only.""" + self.F.ApiKeyFactory.create(active=False) + k = self.F.ApiKeyFactory.create(active=True) + + self.assertEqual(list(self.model.ApiKey.objects.active()), [k]) + + + def test_generate(self): + """Generate classmethod generates an API key from a UUID.""" + u1 = self.F.UserFactory.create() + u2 = self.F.UserFactory.create() + + with patch("moztrap.model.core.models.uuid") as mockuuid: + mockuuid.uuid4.return_value = "foo" + + k = self.model.ApiKey.generate(owner=u1, user=u2) + + self.assertEqual(k.key, "foo") + self.assertEqual(k.owner, u1) + self.assertEqual(k.created_by, u2) + + + def test_generate_default_creator(self): + """Generate classmethod can just take a single user.""" + u1 = self.F.UserFactory.create() + + k = self.model.ApiKey.generate(u1) + + self.assertEqual(k.owner, u1) + self.assertEqual(k.created_by, u1) diff --git a/tests/view/users/test_views.py b/tests/view/users/test_views.py index a4ad62b1..dbe80e6e 100644 --- a/tests/view/users/test_views.py +++ b/tests/view/users/test_views.py @@ -464,3 +464,30 @@ def test_failed_activate(self): ) res.mustcontain("that activation key is not valid") + + + +class CreateApiKeyTest(case.view.AuthenticatedViewTestCase): + """Tests for the create_apikey view.""" + def setUp(self): + """Creating an API key requires a user.""" + super(CreateApiKeyTest, self).setUp() + self.owner = self.F.UserFactory.create() + + + @property + def url(self): + """Shortcut for user-edit url (from where we can create an API key).""" + return reverse("manage_user_edit", kwargs={"user_id": self.owner.id}) + + + def test_post(self): + """POSTing to create_apikey creates a key and redirects to user-edit.""" + self.add_perm("manage_users") + res = self.get().forms["create-apikey-form"].submit() + + self.assertRedirects(res, self.url) + + ak = self.model.ApiKey.objects.get() + self.assertEqual(ak.created_by, self.user) + self.assertEqual(ak.owner, self.owner)