Permalink
Browse files

new resource api for adding suites to a run. fixed sorting performanc…

…e in suite editing.
  • Loading branch information...
camd committed Apr 10, 2013
1 parent 0ec0d6b commit 9f9f492b6d3471129ce36739649f43f97416414b
@@ -1,6 +1,7 @@
-from django.db.models import Max
+from django.db.models import Count
from tastypie.resources import ModelResource, ALL_WITH_RELATIONS
-from tastypie import fields
+from tastypie import http, fields
+from tastypie.exceptions import ImmediateHttpResponse
from tastypie.bundle import Bundle
import json
@@ -9,16 +10,28 @@
from django.http import HttpResponse
from .models import Run, RunCaseVersion, RunSuite, Result
-from ..mtapi import MTResource, MTApiKeyAuthentication
+from ..mtapi import MTResource, MTApiKeyAuthentication, MTAuthorization
from ..core.api import (ProductVersionResource, ProductResource,
ReportResultsAuthorization, UserResource)
from ..environments.api import EnvironmentResource
from ..environments.models import Environment
-from ..library.api import CaseVersionResource, BaseSelectionResource
+from ..library.api import (CaseVersionResource, BaseSelectionResource,
+ SuiteResource)
from ..library.models import CaseVersion, Suite
from ...view.lists.filters import filter_url
+import logging
+logger = logging.getLogger(__name__)
+
+
+class RunSuiteAuthorization(MTAuthorization):
+ """Atypically named permission."""
+
+ @property
+ def permission(self):
+ """This permission should be checked by is_authorized."""
+ return "execution.manage_runs"
class RunCaseVersionResource(ModelResource):
@@ -293,6 +306,61 @@ def obj_create(self, bundle, request=None, **kwargs):
+class RunSuiteResource(MTResource):
+ """
+ Create, Read, Update and Delete capabilities for RunSuite.
+
+ Filterable by suite and run fields.
+ """
+
+ run = fields.ForeignKey(RunResource, 'run')
+ suite = fields.ForeignKey(SuiteResource, 'suite')
+
+ class Meta(MTResource.Meta):
+ queryset = RunSuite.objects.all()
+ fields = ["suite", "run", "order", "id"]
+ filtering = {
+ "suite": ALL_WITH_RELATIONS,
+ "run": ALL_WITH_RELATIONS
+ }
+ authorization = RunSuiteAuthorization()
+
+ @property
+ def model(self):
+ return RunSuite
+
+
+ @property
+ def read_create_fields(self):
+ """run and suite are read-only"""
+ return ["suite", "run"]
+
+
+ def hydrate_suite(self, bundle):
+ """suite is read-only on PUT
+ suite.product must match run.productversion.product on CREATE
+ """
+
+ # CREATE
+ if bundle.request.META['REQUEST_METHOD'] == 'POST':
+ suite_id = self._id_from_uri(bundle.data['suite'])
+ suite = Suite.objects.get(id=suite_id)
+ run_id = self._id_from_uri(bundle.data['run'])
+ run = Run.objects.get(id=run_id)
+ if suite.product.id != run.productversion.product.id:
+ error_message = str(
+ "suite's product must match run's product."
+ )
+ logger.error(
+ "\n".join([error_message, "suite prod: %s, run prod: %s"]),
+ suite.product.id, run.productversion.product.id)
+ raise ImmediateHttpResponse(
+ response=http.HttpBadRequest(error_message))
+
+ return bundle
+
+
+
class SuiteSelectionResource(BaseSelectionResource):
"""
Specialty end-point for an AJAX call from the multi-select widget
@@ -307,10 +375,10 @@ class SuiteSelectionResource(BaseSelectionResource):
class Meta:
queryset = Suite.objects.all().select_related(
"created_by",
- ).annotate(order=Max("runsuites__order"))
+ ).annotate(case_count=Count("cases"))
list_allowed_methods = ['get']
- fields = ["id", "name", "created_by", "runsuites"]
+ fields = ["id", "name", "created_by"]
filtering = {
"product": ALL_WITH_RELATIONS,
"runs": ALL_WITH_RELATIONS,
@@ -324,8 +392,7 @@ def dehydrate(self, bundle):
suite = bundle.obj
bundle.data["suite_id"] = unicode(suite.id)
- bundle.data["case_count"] = suite.cases.count()
+ bundle.data["case_count"] = suite.case_count
bundle.data["filter_cases"] = filter_url("manage_cases", suite)
- bundle.data["order"] = suite.order
return bundle
@@ -491,7 +491,7 @@ class RunSuite(MTModel):
An ordered association between a Run and a Suite.
The only direct impact of RunSuite instances is that they determine which
- RunCaseVersions are created when the run is activated.
+ RunCaseVersions (and in what order) are created when the run is activated.
"""
run = models.ForeignKey(Run, related_name="runsuites")
@@ -1,5 +1,3 @@
-from django.db.models import Max
-from moztrap.model.mtmodel import NotDeletedCount
from tastypie import http, fields
from tastypie.exceptions import ImmediateHttpResponse
from tastypie.resources import ModelResource, ALL, ALL_WITH_RELATIONS
@@ -343,7 +341,7 @@ class Meta:
).prefetch_related(
"tags",
"tags__product",
- "case__suitecases",
+ # "case__suitecases",
)
list_allowed_methods = ['get']
fields = ["id", "name", "latest", "created_by"]
@@ -356,21 +354,6 @@ class Meta:
ordering = ["case"]
-
- def apply_filters(self, request, applicable_filters,
- applicable_excludes={}):
- """
- Workaround to add annotation of order only where we need it.
- There is a bug that you can't annotate through 3 levels.
- """
- req = super(CaseSelectionResource, self).apply_filters(request,
- applicable_filters,
- applicable_excludes)
- if not len(applicable_excludes):
- return req.annotate(case_order=Max("case__suitecases__order"))
- else:
- return req
-
def dehydrate(self, bundle):
"""Add some convenience fields to the return JSON."""
@@ -379,11 +362,6 @@ def dehydrate(self, bundle):
bundle.data["product_id"] = unicode(case.product_id)
bundle.data["product"] = {"id": unicode(case.product_id)}
- if "case__suites" in bundle.request.GET.keys():
- bundle.data["order"] = bundle.obj.case_order
- else:
- bundle.data["order"] = None
-
return bundle
@@ -16,9 +16,9 @@
except NameError:
HMAC_KEYS = {"default": SECRET_KEY}
-# LOGGING["handlers"]["null"] = {
-# 'level': 'DEBUG',
-# 'class': 'django.utils.log.NullHandler',
-# }
-#
-# LOGGING["root"] = {"handlers": ["null"]}
+LOGGING["handlers"]["null"] = {
+ 'level': 'DEBUG',
+ 'class': 'django.utils.log.NullHandler',
+ }
+
+LOGGING["root"] = {"handlers": ["null"]}
View
@@ -14,6 +14,7 @@
v1_api.register(execution.RunResource())
v1_api.register(execution.RunCaseVersionResource())
+v1_api.register(execution.RunSuiteResource())
v1_api.register(execution.ResultResource())
v1_api.register(execution.SuiteSelectionResource())
v1_api.register(library.CaseResource())
View
@@ -41,7 +41,7 @@ class ApiCrudCases(ApiTestCase):
- test_update_without_fk()
- test_update_list_forbidden()
- test_update_fails_without_creds()
- - test_delete_detail_perminant()
+ - test_delete_detail_permanent()
- test_delete_detail_soft()
- test_delete_list_forbidden()
- test_delete_fails_with_wrong_perms()
@@ -0,0 +1,139 @@
+"""
+Tests for RunRunResource api.
+
+"""
+from tests.case.api.crud import ApiCrudCases
+
+import logging
+mozlogger = logging.getLogger('moztrap.test')
+
+
+class RunSuiteResourceTest(ApiCrudCases):
+ """Please see the test suites implemented in tests.suite.api.ApiCrudSuites.
+
+ The following abstract methods must be implemented:
+ - factory(self) (property)
+ - resource_name(self) (property)
+ - permission(self) (property)
+ - new_object_data(self) (property)
+ - backend_object(self, id) (method)
+ - backend_data(self, backend_object) (method)
+ - backend_meta_data(self, backend_object) (method)
+
+ """
+
+ # implementations for abstract methods and properties
+
+ @property
+ def factory(self):
+ """The factory to use to create fixtures of the object under test.
+ """
+ return self.F.RunSuiteFactory()
+
+
+ @property
+ def resource_name(self):
+ """String defining the resource name.
+ """
+ return "runsuite"
+
+
+ @property
+ def permission(self):
+ """String defining the permission required for
+ Create, Update, and Delete.
+ """
+ return "execution.manage_runs"
+
+
+ def order_generator(self):
+ """give an incrementing number for order."""
+ self.__dict__.setdefault("order", 0)
+ self.order += 1
+ return self.order
+
+
+ @property
+ def new_object_data(self):
+ """Generates a dictionary containing the field names and auto-generated
+ values needed to create a unique object.
+
+ The output of this method can be sent in the payload parameter of a
+ POST message.
+ """
+ self.productversion_fixture = self.F.ProductVersionFactory.create()
+ self.suite_fixture = self.F.SuiteFactory.create(
+ product=self.productversion_fixture.product)
+ self.run_fixture = self.F.RunFactory.create(
+ productversion=self.productversion_fixture)
+
+ fields = {
+ u"suite": unicode(
+ self.get_detail_url("suite", str(self.suite_fixture.id))),
+ u"run": unicode(
+ self.get_detail_url("run", str(self.run_fixture.id))
+ ),
+ u"order": self.order_generator(),
+ }
+ return fields
+
+
+ def backend_object(self, id):
+ """Returns the object from the backend, so you can query it's values in
+ the database for validation.
+ """
+ return self.model.RunSuite.everything.get(id=id)
+
+
+ def backend_data(self, backend_obj):
+ """Query's the database for the object's current values. Output is a
+ dictionary that should match the result of getting the object's detail
+ via the API, and can be used to verify API output.
+
+ Note: both keys and data should be in unicode
+ """
+ return {
+ u"id": unicode(str(backend_obj.id)),
+ u"resource_uri": unicode(
+ self.get_detail_url(self.resource_name, str(backend_obj.id))),
+ u"suite": unicode(
+ self.get_detail_url("suite", str(backend_obj.suite.id))),
+ u"run": unicode(
+ self.get_detail_url("run", str(backend_obj.run.id))),
+ u"order": backend_obj.order,
+ }
+
+
+ @property
+ def read_create_fields(self):
+ """List of fields that are required for create but read-only for update."""
+ return ["run", "suite"]
+
+ # overrides from crud.py
+
+ # additional test suites, if any
+
+ # validation suites
+
+ def test_create_mismatched_product_error(self):
+ """error if run.product does not match suite.product"""
+
+ mozlogger.info("test_create_mismatched_product_error")
+
+ fields = self.new_object_data
+ product = self.F.ProductFactory()
+ self.suite_fixture.product = product
+ self.suite_fixture.save()
+
+ # do post
+ res = self.post(
+ self.get_list_url(self.resource_name),
+ params=self.credentials,
+ payload=fields,
+ status=400,
+ )
+
+ error_message = str(
+ "suite's product must match run's product."
+ )
+ self.assertEqual(res.text, error_message)
@@ -120,7 +120,7 @@ def available_param(self):
return "{0}__ne".format(self.included_param)
- def get_exp_obj(self, cv, order=None):
+ def get_exp_obj(self, cv):
"""Return an expected caseselection object with fields filled."""
return {
u"case": unicode(
@@ -130,7 +130,6 @@ def get_exp_obj(self, cv, order=None):
u"id": unicode(cv.id),
u"latest": True,
u"name": unicode(cv.name),
- u"order": order,
u"product": {
u"id": unicode(cv.productversion.product_id)
},
@@ -217,7 +216,6 @@ def test_included_for_two_included(self):
exp_objects = [
self.get_exp_obj(
cv,
- order=sc.order,
) for cv, sc in [
(data["cv1"], data["sc1"]),
(data["cv2"], data["sc2"]),
@@ -238,7 +236,6 @@ def _setup_for_one_included_one_not(self):
sc1 = self.F.SuiteCaseFactory.create(
case=cv1.case,
suite=suite,
- order=0,
)
return {
"cv1": cv1,
@@ -268,7 +265,6 @@ def test_included_for_one_included_one_not(self):
exp_objects = [
self.get_exp_obj(
cv,
- order=sc.order,
) for cv, sc in [(data["cv1"], data["sc1"])]
]
Oops, something went wrong.

0 comments on commit 9f9f492

Please sign in to comment.