Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

API for environments, including PATCH for combinatorics #24

Merged
merged 5 commits into from

3 participants

@klrmn

No description provided.

@camd camd commented on the diff
moztrap/model/environments/api.py
((72 lines not shown))
+ request,
+ request.raw_post_data,
+ format=request.META.get('CONTENT_TYPE', 'application/json'))
+
+ # verify input
+ categories = deserialized.pop('categories', [])
+ if not categories or not isinstance(categories, list):
+ error_msg = "PATCH request must contain categories list."
+ logger.error(error_msg)
+ raise ImmediateHttpResponse(
+ response=http.HttpBadRequest(error_msg))
+
+
+ # do the combinatorics
+ elem_lists = []
+ for cat in categories:
@camd Owner
camd added a note

we could use some comments here explaining how this works.

@klrmn
klrmn added a note

i've plumbed in some comments locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@satdav

this is ready to be merged if you ask me

moztrap/model/environments/api.py
((107 lines not shown))
+ else:
+ # don't worry about this,
+ # it'll act like a list of categories
+ pass # pragma: no cover
+ else:
+ error_msg = "categories list must contain resource uris or hashes."
+ logger.error(error_msg)
+ raise ImmediateHttpResponse(
+ response=http.HttpBadRequest(error_msg))
+
+ elem_lists.append(elem_list)
+
+ combinatorics = itertools.product(*elem_lists)
+
+ # do the creation
+ with transaction.commit_on_success():
@camd Owner
camd added a note

how do we handle if the "environment" already exists? does it use the existing one? or will this be creating a new one. I think that's happening in the self.obj_create() right? Is there a way to catch that? either in obj_create or here?

@klrmn
klrmn added a note

i thought that ModelResource.obj_create (https://github.com/toastdriven/django-tastypie/blob/master/tastypie/resources.py#L2099) used ModelXXX.get_or_create() but i appear to be mistaken (perhaps i was thinking that about ModelResource.obj_update()).

this may be a problem in a number of places. the problem is, very few places in the app actually have uniqueness assumptions. i can't imagine how we would unique on environments unless profile were required, for instance (because two different profiles could certainly have the same set of elements). What is the use case again for an environment with a blank profile?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@klrmn

i'll commit the comments when we figure out how to handle the uniqueness issues

@camd camd merged commit 51d37f3 into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 15, 2013
  1. @klrmn
Commits on Mar 16, 2013
  1. @klrmn

    basic combinatorics in patch

    klrmn authored
  2. @klrmn

    environments need put after all

    klrmn authored
Commits on Mar 17, 2013
  1. @klrmn
Commits on Mar 22, 2013
  1. @klrmn
This page is out of date. Refresh to see the latest.
View
67 docs/userguide/api/environments.rst
@@ -1,6 +1,12 @@
Environment API
===============
+Environments do not behave in quite the same way in the API as they do in
+the Web UI. In the API, create Categories and their child Elements first,
+then create a Profile for which you can create Environments whose elements
+must each belong to a separate profile.
+
+
Profile
-------
@@ -87,3 +93,64 @@ Filtering
.. sourcecode:: http
GET /api/v1/environment/?format=json&elements=5
+
+.. http:get:: /api/v1/environment/<id>
+.. http:post:: /api/v1/environment
+
+Required Fields
+^^^^^^^^^^^^^^^
+
+ :profile: A resource uri to the parent Profile.
+ :elements: A list of element resource uri's.
+
+.. note:: Each element must be from a separate category.
+
+.. http:delete:: /api/v1/environment/<id>
+.. http:put:: /api/v1/environment/<id>
+
+.. http:patch:: /api/v1/environment
+
+ The `PATCH` command is being overloaded to provide combinatorics
+ services to create `environments` out of `elements` contained by
+ `categories`.
+
+ To create environments for all of the combinations of elements in
+ the listed categories:
+
+ .. sourcecode:: python
+
+ data={
+ u'profile': u'/api/v1/profile/1',
+ u'categories': [u'/api/v1/category/1', ...]
+ }
+
+ You may also do combinatorics with partial sets of elements from
+ the categories by using dictionaries with 'include' and 'exclude' keys.
+
+ .. sourcecode:: python
+
+ data={
+ u'profile': u'/api/v1/profile/1',
+ u'categories': [
+ {
+ u'category': u'/api/v1/category/1',
+ u'exclude': [u'/api/v1/element/1']
+ },
+ {
+ u'category': u'/api/v1/category/2',
+ u'include': [
+ u'/api/v1/element/4',
+ u'/api/v1/element/5'
+ ]
+ },
+ {
+ u'category': u'/api/v1/category/3'
+ }
+ ]
+ }
+
+ .. note::
+
+ The included or excluded elements must be members of the category
+ they accompany. If both include and exclude are sent with the same
+ category, exclude will be performed.
View
29 docs/userguide/api/index.rst
@@ -16,33 +16,46 @@ The general format for all rest endpoints is:
Return a list of objects
+ **limit** (optional) Defaults to 20 items, but can be set higher or lower.
+ 0 will return all records, but may run afoul of
**Example request**:
.. sourcecode:: http
GET /api/v1/product/?format=json&limit=50
-.. http:get:: /api/v1/<object_type/<id>/
+.. http:get:: /api/v1/<object_type>/<id>/
Return a single object
-.. http:post:: /api/v1/<object_type>/
+.. https:post:: /api/v1/<object_type>/
- Create one or more items. **requires** :ref:`API key<api-key>`
+ Create one or more items.
-.. http:put:: /api/v1/<object_type>/<id>
+ **requires** :ref:`API key<api-key>`
+ **requires** :ref:`username`
- Update one item. **requires** :ref:`API key<api-key>`
+ If sending the fields as data, the data must be sent as json, with
+ Content-Type application/json in the headers.
-.. http:delete:: /api/v1/<object_type>/<id>
+.. https:put:: /api/v1/<object_type>/<id>
- Delete one item. **requires** :ref:`API key<api-key>`
+ Update one item.
+ **requires** :ref:`API key<api-key>`
+ **requires** :ref:`username`
+
+.. https:delete:: /api/v1/<object_type>/<id>
+
+ Delete one item.
+ **requires** :ref:`API key<api-key>`
+ **requires** :ref:`username`
.. note::
* POST does not replace the whole list of items, it only creates new ones
* DELETE on a list is not supported
* PUT to a list is not supported
+ * commands that make changes may need to be sent to https, not http.
Query Parameters
@@ -59,8 +72,6 @@ Some fields are universal to all requests and :ref:`Object Types<object-types>`:
* **format** (required) The API **always** requires a value of ``json`` for
this field.
-* **limit** (optional) Defaults to 20 items, but can be set higher or lower.
- 0 will return all records.
.. note::
View
133 moztrap/model/environments/api.py
@@ -1,9 +1,13 @@
from tastypie import fields
+from tastypie import http
from tastypie.resources import ModelResource, ALL, ALL_WITH_RELATIONS
+from tastypie.exceptions import ImmediateHttpResponse
from ..mtapi import MTResource, MTAuthorization
from .models import Profile, Environment, Element, Category
+import logging
+logger = logging.getLogger(__name__)
class EnvironmentAuthorization(MTAuthorization):
@@ -54,6 +58,7 @@ class Meta(MTResource.Meta):
"name": ALL,
}
+
@property
def model(self):
"""Model class related to this resource."""
@@ -82,20 +87,132 @@ def model(self):
"""Model class related to this resource."""
return Element
+
@property
def read_create_fields(self):
- """List of fields that are required for create but read-only for update."""
+ """List of fields that are required for create
+ but read-only for update."""
return ["category"]
-class EnvironmentResource(ModelResource):
- """Return a list of environments"""
+class EnvironmentResource(MTResource):
+ """Create, Read and Delete capabilities for environments"""
- elements = fields.ToManyField(ElementResource, "elements", full=True)
+ elements = fields.ToManyField(ElementResource, "elements")
+ profile = fields.ForeignKey(ProfileResource, "profile")
- class Meta:
+ class Meta(MTResource.Meta):
queryset = Environment.objects.all()
- list_allowed_methods = ['get']
- fields = ["id"]
- filtering = {"elements": ALL}
+ list_allowed_methods = ['get', 'post', 'patch']
+ detail_allowed_methods = ['get', 'put', 'delete']
+ fields = ["id", "profile", "elements"]
+ filtering = {
+ "elements": ALL,
+ "profile": ALL_WITH_RELATIONS,
+ }
+ ordering = ["id", "profile"]
+
+
+ @property
+ def model(self):
+ """Model class related to this resource."""
+ return Environment
+
+
+ def hydrate_m2m(self, bundle):
+ """Validate the elements,
+ which should each belong to separate categories."""
+
+ bundle = super(EnvironmentResource, self).hydrate_m2m(bundle)
+ elem_categories = [elem.data['category'] for elem in
+ bundle.data['elements']]
+ if len(set(elem_categories)) != len(bundle.data['elements']):
+ error_msg = "Elements must each belong to a different Category."
+ logger.error(error_msg)
+ raise ImmediateHttpResponse(
+ response=http.HttpBadRequest(error_msg))
+ return bundle
+
+
+ def patch_list(self, request, **kwargs):
+ """
+ Since there is no RESTful way to do what we want to do, and since
+ ``PATCH`` is poorly defined with regards to RESTfulness, we are
+ overloading ``PATCH`` to take a single request that performs
+ combinatorics and creates multiple objects.
+ """
+ import itertools
+ from django.db import transaction
+ from tastypie.utils import dict_strip_unicode_keys
+
+ deserialized = self.deserialize(
+ request,
+ request.raw_post_data,
+ format=request.META.get('CONTENT_TYPE', 'application/json'))
+
+ # verify input
+ categories = deserialized.pop('categories', [])
+ if not categories or not isinstance(categories, list):
+ error_msg = "PATCH request must contain categories list."
+ logger.error(error_msg)
+ raise ImmediateHttpResponse(
+ response=http.HttpBadRequest(error_msg))
+
+
+ # do the combinatorics
+ elem_lists = []
+ for cat in categories:
@camd Owner
camd added a note

we could use some comments here explaining how this works.

@klrmn
klrmn added a note

i've plumbed in some comments locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ # do some type validation / variation
+ if isinstance(cat, basestring):
+ # simple case of create all the combinations
+ cat = Category.objects.filter(id=self._id_from_uri(cat))
+ elem_list = Element.objects.filter(category=cat)
+ elif isinstance(cat, dict):
+ # we must be working with at least one partial category
+ category = Category.objects.filter(
+ id=self._id_from_uri(cat['category']))
+ elem_list = Element.objects.filter(category=category)
+ if 'exclude' in cat:
+ # exclude some element(s) from the combinations
+ exclude_uris = cat['exclude']
+ exclude_ids = [int(
+ self._id_from_uri(x)) for x in exclude_uris]
+ elem_list = [elem for elem in elem_list
+ if elem.id not in exclude_ids]
+ elif 'include' in cat:
+ # include only a few elements in the combinations
+ include_uris = cat['include']
+ include_ids = [int(
+ self._id_from_uri(x)) for x in include_uris]
+ elem_list = [elem for elem in elem_list
+ if elem.id in include_ids]
+ else:
+ # don't worry about this,
+ # it'll act like a list of categories
+ pass # pragma: no cover
+ else:
+ error_msg = "categories list must contain resource uris or hashes."
+ logger.error(error_msg)
+ raise ImmediateHttpResponse(
+ response=http.HttpBadRequest(error_msg))
+
+ # save off the elements from this category that will be used
+ elem_lists.append(elem_list)
+
+ # create all the combinations of elements from categories
+ combinatorics = itertools.product(*elem_lists)
+
+ # do the creation
+ with transaction.commit_on_success():
+ for combo in combinatorics:
+ deserialized['elements'] = combo
+ bundle = self.build_bundle(
+ data=dict_strip_unicode_keys(deserialized))
+ bundle.request.META['REQUEST_METHOD'] = 'PATCH'
+ self.is_valid(bundle, request)
+ self.obj_create(bundle, request=request)
+
+ # don't try to reply with data, the request doesn't
+ # really match the results.
+ return http.HttpAccepted()
View
4 tests/case/api/crud.py
@@ -352,7 +352,7 @@ def _test_filter_list_by(self, key, value, expected_number_of_results):
self._test_filter_list_by(u'product', u'', 3)
"""
- res = self.get_list(params={key: value})
+ res = self.get_list(params={key: value, u'limit': 100})
objects = res.json["objects"]
# validate
@@ -362,7 +362,7 @@ def _test_filter_list_by(self, key, value, expected_number_of_results):
# can't use assertEqual here because for foriegn keys,
# the filter must be an id
# but the value in the response is an uri
- self.assertTrue(value in obj[key])
+ self.assertTrue(str(value) in obj[key])
else:
# case for value being None
self.assertEqual(value, obj[key])
View
14 tests/model/core/api/test_productversion_env_resource.py
@@ -52,17 +52,17 @@ def test_productversionenvironments_list(self):
exp_objects.append({
u"codename": unicode(pv.codename),
u'environments': [{
- u'elements': [{
- u'category': unicode(
- self.get_detail_url("category", category.id)),
- u'id': unicode(element.id),
- u'name': u'OS X',
- u'resource_uri': unicode(self.get_detail_url(
+ u'elements': [
+ unicode(self.get_detail_url(
"element",
element.id,
)),
- }],
+ ],
u'id': unicode(envs[0].id),
+ u'profile': unicode(self.get_detail_url(
+ "profile",
+ envs[0].profile.id
+ )),
u'resource_uri': unicode(self.get_detail_url(
"environment",
envs[0].id,
View
2  tests/model/environments/api/test_element_resource.py
@@ -1,5 +1,5 @@
"""
-Tests for EnvironmentResource api.
+Tests for ElementResource api.
"""
View
346 tests/model/environments/api/test_environment_resource.py
@@ -3,16 +3,18 @@
"""
-from tests import case
+from tests.case.api.crud import ApiCrudCases
+import logging
+logger = logging.getLogger("moztrap.test")
-class EnvironmentResourceTest(case.api.ApiTestCase):
+class EnvironmentResourceTest(ApiCrudCases):
@property
def factory(self):
"""The model factory for this object."""
- return self.F.EnvironmentFactory
+ return self.F.EnvironmentFactory()
@property
@@ -20,46 +22,306 @@ def resource_name(self):
return "environment"
- def test_environment_list(self):
- """Get a list of existing environments"""
- envs = self.F.EnvironmentFactory.create_full_set(
- {"OS": ["OS X"]})
- element = envs[0].elements.get()
- category = element.category
+ @property
+ def permission(self):
+ """The permissions needed to modify this object type."""
+ return "environments.manage_environments"
+
+
+ @property
+ def new_object_data(self):
+ """Generates a dictionary containing the field names and auto-generated
+ values needed to create a unique object.
- res = self.get_list()
+ The output of this method can be sent in the payload parameter of a
+ POST message.
+ """
- act_meta = res.json["meta"]
- exp_meta = {
- "limit": 20,
- "next": None,
- "offset": 0,
- "previous": None,
- "total_count": 1,
+ self.profile_fixture = self.F.ProfileFactory()
+ self.category_fixture1 = self.F.CategoryFactory(name="A")
+ self.category_fixture2 = self.F.CategoryFactory(name="B")
+ self.category_fixture3 = self.F.CategoryFactory(name="C")
+ self.element_fixture1 = self.F.ElementFactory(category=self.category_fixture1, name="A 2")
+ self.element_fixture2 = self.F.ElementFactory(category=self.category_fixture2, name="B 2")
+ self.element_fixture3 = self.F.ElementFactory(category=self.category_fixture3, name="C 2")
+ self.element_fixture_list = [
+ self.element_fixture1, self.element_fixture2, self.element_fixture3]
+
+ return {
+ u"profile": unicode(
+ self.get_detail_url("profile", str(self.profile_fixture.id))),
+ u"elements": [unicode(
+ self.get_detail_url(
+ "element", str(elem.id))
+ ) for elem in self.element_fixture_list],
}
- self.assertEquals(act_meta, exp_meta)
-
- act_objects = res.json["objects"]
- exp_objects = []
-
- exp_objects.append({
- u'elements': [{
- u'category': unicode(
- self.get_detail_url("category", category.id)),
- u'id': unicode(element.id),
- u'name': u'OS X',
- u'resource_uri': unicode(self.get_detail_url(
- "element",
- element.id,
- )),
- }],
- u'id': unicode(envs[0].id),
- u'resource_uri': unicode(self.get_detail_url(
- "environment",
- envs[0].id,
- )),
- })
-
- self.maxDiff = None
- self.assertEqual(exp_objects, act_objects)
+
+ 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.Environment.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"profile": unicode(self.get_detail_url("profile", str(backend_obj.profile.id))),
+ u"elements": [unicode(
+ self.get_detail_url("element", str(elem.id))
+ ) for elem in backend_obj.elements.all()],
+ u"resource_uri": unicode(
+ self.get_detail_url(self.resource_name, str(backend_obj.id))),
+ }
+
+
+ def test_elements_must_be_from_different_categories(self):
+ """A post with two elements from the same category should error."""
+ logger.info("test_elements_must_be_from_different_categories")
+
+ # get data for creation & munge it
+ fields = self.new_object_data
+ self.element_fixture2.category = self.element_fixture1.category
+ self.element_fixture2.save()
+
+ # do the create
+ res = self.post(
+ self.get_list_url(self.resource_name),
+ params=self.credentials,
+ payload=fields,
+ status=400,
+ )
+
+ error_msg = "Elements must each belong to a different Category."
+ self.assertEqual(res.text, error_msg)
+
+
+ def test_basic_combinatorics_patch(self):
+ """A Patch request with profile and categories should do combinatorics
+ on the categories and create environments."""
+ logger.info("test_basic_combinatorics_patch")
+
+ fields = self.new_object_data
+
+ # create more elements for each category
+ for x in range(2):
+ self.F.ElementFactory(category=self.category_fixture1, name="A %s" % x)
+ self.F.ElementFactory(category=self.category_fixture2, name="B %s" % x)
+ self.F.ElementFactory(category=self.category_fixture3, name="C %s" % x)
+
+ # modify fields to send categories rather than elements
+ fields.pop('elements')
+ fields['categories'] = [
+ unicode(self.get_detail_url(
+ "category", str(self.category_fixture1.id))),
+ unicode(self.get_detail_url(
+ "category", str(self.category_fixture2.id))),
+ unicode(self.get_detail_url(
+ "category", str(self.category_fixture3.id))),
+ ]
+
+ # do the create
+ res = self.patch(
+ self.get_list_url(self.resource_name),
+ params=self.credentials,
+ payload=fields,
+ )
+
+ # check that it made the right number of environments
+ self._test_filter_list_by(u'profile', self.profile_fixture.id, 27)
+
+
+ def test_patch_without_categories_error(self):
+ """'categories' must be provided in PATCH."""
+ logger.info("test_patch_without_categories_error")
+
+ fields = self.new_object_data
+
+ # do the create
+ res = self.patch(
+ self.get_list_url(self.resource_name),
+ params=self.credentials,
+ payload=fields,
+ status=400,
+ )
+
+ error_msg = "PATCH request must contain categories list."
+ self.assertEqual(res.text, error_msg)
+
+
+ def test_patch_categories_not_list_error(self):
+ """'categories' must be a list in PATCH."""
+ logger.info("test_patch_categories_not_list_error")
+
+ fields = self.new_object_data
+ fields.pop("elements")
+ fields[u'categories'] = unicode(
+ self.get_detail_url("category", str(self.category_fixture1.id)))
+
+ # do the create
+ res = self.patch(
+ self.get_list_url(self.resource_name),
+ params=self.credentials,
+ payload=fields,
+ status=400,
+ )
+
+ error_msg = "PATCH request must contain categories list."
+ self.assertEqual(res.text, error_msg)
+
+
+ def test_patch_categories_list_not_string_or_hash_error(self):
+ """'categories' must be a list in PATCH."""
+ logger.info("test_patch_categories_list_not_string_or_hash_error")
+
+ fields = self.new_object_data
+ fields.pop("elements")
+ fields[u'categories'] = [1, 2, 3]
+
+ # do the create
+ res = self.patch(
+ self.get_list_url(self.resource_name),
+ params=self.credentials,
+ payload=fields,
+ status=400,
+ )
+
+ error_msg = "categories list must contain resource uris or hashes."
+ self.assertEqual(res.text, error_msg)
+
+
+ def test_patch_with_exclude(self):
+ """Combinatorics excluding some elements."""
+ logger.info("test_patch_with_exclude")
+
+ fields = self.new_object_data
+
+ # create more elements for each category
+ for x in range(2):
+ self.F.ElementFactory(category=self.category_fixture1, name="A %s" % x)
+ self.F.ElementFactory(category=self.category_fixture2, name="B %s" % x)
+ self.F.ElementFactory(category=self.category_fixture3, name="C %s" % x)
+
+ # modify fields to send categories rather than elements
+ fields.pop('elements')
+ fields['categories'] = [
+ {
+ u'category': unicode(self.get_detail_url(
+ "category", str(self.category_fixture1.id))),
+ u'exclude': [unicode(self.get_detail_url(
+ "element", str(self.element_fixture1.id))), ],
+ },
+ {
+ u'category': unicode(self.get_detail_url(
+ "category", str(self.category_fixture2.id))),
+ u'exclude': [unicode(self.get_detail_url(
+ "element", str(self.element_fixture2.id))), ],
+ },
+ {
+ u'category': unicode(self.get_detail_url(
+ "category", str(self.category_fixture3.id))),
+ u'exclude': [unicode(self.get_detail_url(
+ "element", str(self.element_fixture3.id))), ],
+ }, ]
+
+ # do the create
+ res = self.patch(
+ self.get_list_url(self.resource_name),
+ params=self.credentials,
+ payload=fields,
+ )
+
+ # check that it made the right number of environments
+ self._test_filter_list_by(u'profile', self.profile_fixture.id, 8)
+
+
+ def test_patch_with_include(self):
+ """Combinatorics including some elements."""
+ logger.info("test_patch_with_include")
+
+ fields = self.new_object_data
+
+ # create more elements for each category
+ for x in range(2):
+ self.F.ElementFactory(category=self.category_fixture1, name="A %s" % x)
+ self.F.ElementFactory(category=self.category_fixture2, name="B %s" % x)
+ self.F.ElementFactory(category=self.category_fixture3, name="C %s" % x)
+
+ # modify fields to send categories rather than elements
+ fields.pop('elements')
+ fields['categories'] = [
+ {
+ u'category': unicode(self.get_detail_url(
+ "category", str(self.category_fixture1.id))),
+ u'include': [unicode(self.get_detail_url(
+ "element", str(self.element_fixture1.id))), ],
+ },
+ {
+ u'category': unicode(self.get_detail_url(
+ "category", str(self.category_fixture2.id))),
+ u'include': [unicode(self.get_detail_url(
+ "element", str(self.element_fixture2.id))), ],
+ },
+ {
+ u'category': unicode(self.get_detail_url(
+ "category", str(self.category_fixture3.id))),
+ u'include': [unicode(self.get_detail_url(
+ "element", str(self.element_fixture3.id))), ],
+ }, ]
+
+ # do the create
+ res = self.patch(
+ self.get_list_url(self.resource_name),
+ params=self.credentials,
+ payload=fields,
+ )
+
+ # check that it made the right number of environments
+ self._test_filter_list_by(u'profile', self.profile_fixture.id, 1)
+
+
+ def test_patch_no_include_no_exclude(self):
+ """Sending hashes without include or exclude should do the same as
+ sending regular uri strings."""
+ logger.info("test_patch_no_include_no_exclude")
+
+ fields = self.new_object_data
+
+ # create more elements for each category
+ for x in range(2):
+ self.F.ElementFactory(category=self.category_fixture1, name="A %s" % x)
+ self.F.ElementFactory(category=self.category_fixture2, name="B %s" % x)
+ self.F.ElementFactory(category=self.category_fixture3, name="C %s" % x)
+
+ # modify fields to send categories rather than elements
+ fields.pop('elements')
+ fields['categories'] = [
+ {
+ u'category': unicode(self.get_detail_url(
+ "category", str(self.category_fixture1.id))),
+ },
+ {
+ u'category': unicode(self.get_detail_url(
+ "category", str(self.category_fixture2.id))),
+ },
+ {
+ u'category': unicode(self.get_detail_url(
+ "category", str(self.category_fixture3.id))),
+ }, ]
+
+ # do the create
+ res = self.patch(
+ self.get_list_url(self.resource_name),
+ params=self.credentials,
+ payload=fields,
+ )
+
+ # check that it made the right number of environments
+ self._test_filter_list_by(u'profile', self.profile_fixture.id, 27)
View
16 tests/model/execution/api/test_run.py
@@ -96,22 +96,18 @@ def test_run_by_id_shows_env_detail(self):
exp_objects = {
u'elements': [
- {u'category': unicode(
- self.get_detail_url(
- "category",
- envs[0].elements.get().category.id)),
- u'resource_uri': unicode(self.get_detail_url(
+ unicode(self.get_detail_url(
"element",
envs[0].elements.get().id),
- ),
- u'id': unicode(envs[0].elements.get().id),
- u'name': u'OS X'
- }],
+ )],
u'id': unicode(envs[0].id),
+ u'profile': unicode(self.get_detail_url(
+ "profile", envs[0].profile.id
+ )),
u'resource_uri': unicode(self.get_detail_url(
"environment",
envs[0].id),
- )
+ ),
}
self.assertEqual(unicode(r.name), res.json["name"], res.json)
Something went wrong with that request. Please try again.