Skip to content
Snippets Groups Projects
Commit 525914b5 authored by Martin Burchell's avatar Martin Burchell
Browse files

Merge branch 'add_multiple_terms_api' into tagging_frontend

Conflicts:
	django/website/rest_api/tests/free_tagging_tests.py
	django/website/transport/tests/free_tagging_tests.py
parents 5b4df219 c262b4e1
No related branches found
No related tags found
1 merge request!46Tagging frontend
Showing
with 265 additions and 104 deletions
class ItemTermException(Exception):
pass
from django.db import models
from django.dispatch.dispatcher import receiver
from django.utils import timezone
from taxonomies.models import Term
from exceptions import ItemTermException
class DataLayerModel(models.Model):
......@@ -23,8 +23,7 @@ class Message(DataLayerModel):
terms = models.ManyToManyField(Term, related_name="items")
network_provider = models.CharField(max_length=200, blank=True)
def apply_term(self, term):
# TODO: test this
def apply_terms(self, terms):
""" Add or replace value of term.taxonomy for current Item
If the Item has no term in the taxonomy
......@@ -37,10 +36,22 @@ class Message(DataLayerModel):
# This should really be built out with an explicity through model
# in taxonomies, with a generic foreign ken to the content type
# being classified, then this logic could live there.
if term.taxonomy.is_optional:
self.delete_all_terms(term.taxonomy)
if isinstance(terms, Term):
terms = [terms]
self.terms.add(term)
taxonomy = terms[0].taxonomy
if not all(t.taxonomy == taxonomy for t in terms):
raise ItemTermException("Terms cannot be applied from different taxonomies")
if taxonomy.is_optional:
if len(terms) > 1:
message = "Taxonomy '%s' does not support multiple terms" % taxonomy
raise ItemTermException(message)
self.delete_all_terms(taxonomy)
self.terms.add(*terms)
def delete_all_terms(self, taxonomy):
for term in self.terms.filter(taxonomy=taxonomy):
......
......@@ -6,6 +6,7 @@ from django.utils import timezone
from factories import ItemFactory
from ..models import Item
from ..exceptions import ItemTermException
from taxonomies.tests.factories import TermFactory, TaxonomyFactory
......@@ -117,31 +118,75 @@ def test_last_modified_date_on_other_item_not_updated(
@pytest.mark.django_db
def test_apply_term_replaces_term_for_categories():
def test_apply_terms_replaces_term_for_categories():
item = ItemFactory()
taxonomy = TaxonomyFactory() # Ensure multiplicity = optional
term1 = TermFactory(taxonomy=taxonomy)
term2 = TermFactory(taxonomy=taxonomy)
assert taxonomy.is_optional
item.apply_term(term1)
item.apply_terms(term1)
assert list(item.terms.all()) == [term1]
item.apply_term(term2)
item.apply_terms(term2)
assert list(item.terms.all()) == [term2]
@pytest.mark.xfail
# I'm putting this here to explain some of my thinking.
@pytest.mark.django_db
def test_apply_term_adds_term_for_tags():
def test_apply_terms_adds_term_for_tags():
item = ItemFactory()
taxonomy = TaxonomyFactory() # Ensure multiplicity == multiple
taxonomy = TaxonomyFactory(multiplicity='multiple')
term1 = TermFactory(taxonomy=taxonomy)
term2 = TermFactory(taxonomy=taxonomy)
assert taxonomy.is_multiple
assert not taxonomy.is_optional
item.apply_term(term1)
item.apply_terms(term1)
assert list(item.terms.all()) == [term1]
item.apply_term(term2)
assert set(item.terms.all()) == set([term1, term2])
item.apply_terms(term2)
assert term1 in item.terms.all()
assert term2 in item.terms.all()
@pytest.mark.django_db
def test_apply_terms_adds_multiple_terms():
item = ItemFactory()
taxonomy = TaxonomyFactory(multiplicity='multiple')
term1 = TermFactory(taxonomy=taxonomy)
term2 = TermFactory(taxonomy=taxonomy)
item.apply_terms((term1, term2))
assert term1 in item.terms.all()
assert term2 in item.terms.all()
@pytest.mark.django_db
def test_applying_multiple_terms_raises_exception_if_not_multiple():
item = ItemFactory()
taxonomy = TaxonomyFactory(multiplicity='optional')
term1 = TermFactory(taxonomy=taxonomy)
term2 = TermFactory(taxonomy=taxonomy)
with pytest.raises(ItemTermException) as excinfo:
item.apply_terms((term1, term2))
expected_message = "Taxonomy '%s' does not support multiple terms" % (
taxonomy)
assert excinfo.value.message == expected_message
@pytest.mark.django_db
def test_applied_terms_must_be_from_same_taxonomy():
item = ItemFactory()
taxonomy1 = TaxonomyFactory()
taxonomy2 = TaxonomyFactory()
term1 = TermFactory(taxonomy=taxonomy1)
term2 = TermFactory(taxonomy=taxonomy2)
term3 = TermFactory(taxonomy=taxonomy1)
with pytest.raises(ItemTermException) as excinfo:
item.apply_terms((term1, term2, term3))
expected_message = "Terms cannot be applied from different taxonomies"
assert excinfo.value.message == expected_message
......@@ -49,7 +49,7 @@ def item():
def categorize_item(item, term):
request = APIRequestFactory().post("", term)
view = ItemViewSet.as_view(actions={'post': 'add_term'})
view = ItemViewSet.as_view(actions={'post': 'add_terms'})
return view(request, item_pk=item['id'])
......@@ -78,12 +78,23 @@ def test_categorize_item_returns_the_categorized_item(term, item):
@pytest.mark.django_db
def test_categorize_item_fails_gracefully_if_term_not_found(item):
def test_categorize_item_fails_gracefully_if_taxonomy_not_found(item):
response = categorize_item(
item,
{'taxonomy': 'unknown-slug', 'name': 'unknown-term'},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.data['detail'] == "Taxonomy matching query does not exist."
@pytest.mark.django_db
def test_categorize_item_fails_gracefully_if_term_not_found(item, category):
response = categorize_item(
item,
{'taxonomy': category['slug'], 'name': 'unknown-term'},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.data['detail'] == "Term matching query does not exist."
......
......@@ -6,6 +6,7 @@ from rest_framework.test import APIRequestFactory
from rest_framework import status
from .item_create_view_tests import create_item
from .taxonomy_and_term_create_tests import create_category
from ..views import ItemViewSet
......@@ -23,7 +24,7 @@ def get_item(id):
def get_add_free_terms_response(item_id, terms):
request = APIRequestFactory().post('', terms)
view = ItemViewSet.as_view(actions={'post': 'add_free_terms'})
view = ItemViewSet.as_view(actions={'post': 'add_terms'})
response = view(request, item_pk=item_id)
return response
......@@ -38,8 +39,13 @@ def add_free_terms_to_item(item_id, terms):
@pytest.mark.django_db
def test_multiple_new_terms_applied_to_item(item):
taxonomy = create_category(name='test tags',
vocabulary='open',
multiplicity='multiple').data
slug = taxonomy['slug']
terms = {
'taxonomy': 'tags',
'taxonomy': slug,
'name': ['Monrovia', 'age 35-40'],
}
......@@ -47,15 +53,14 @@ def test_multiple_new_terms_applied_to_item(item):
updated_item = get_item(item['id']).data
taxonomy_terms = {}
new_terms = []
for term in updated_item['terms']:
if term['taxonomy'] not in taxonomy_terms:
taxonomy_terms[term['taxonomy']] = []
taxonomy_terms[term['taxonomy']].append(term['name'])
if term['taxonomy'] == slug:
new_terms.append(term['name'])
assert 'tags' in taxonomy_terms
assert 'Monrovia' in taxonomy_terms['tags']
assert 'age 35-40' in taxonomy_terms['tags']
assert len(new_terms) > 0, "No terms created with taxonomy '%s'" % slug
assert 'Monrovia' in new_terms
assert 'age 35-40' in new_terms
@pytest.mark.django_db
......
......@@ -62,7 +62,7 @@ def test_filter_by_id_list():
@pytest.mark.django_db
def test_filter_by_term():
taxonomy = create_category('taxonomy').data
taxonomy = create_category(name='taxonomy').data
term = add_term(taxonomy=taxonomy['slug'], name='term').data
items = [create_item(body='item %d' % i).data for i in range(3)]
......@@ -101,7 +101,7 @@ def test_filter_by_multiple_terms():
@pytest.mark.django_db
def test_filter_by_term_works_when_term_name_includes_colon():
taxonomy = create_category('taxonomy').data
taxonomy = create_category(name='taxonomy').data
term = add_term(taxonomy=taxonomy['slug'], name='term:with:colon').data
item = create_item(body='item 1').data
categorize_item(item, term)
......
......@@ -51,7 +51,7 @@ def test_item_terms_not_affected_by_update():
item = create_item(body='What is the cuse of Ebola?').data
id = item['id']
questions_category = create_category("Test Ebola Questions").data
questions_category = create_category(name="Test Ebola Questions").data
categorize_item(item, term_for(questions_category, 'Vaccine'))
......@@ -72,9 +72,9 @@ def test_item_terms_can_be_updated():
item = create_item(body='What is the cuse of Ebola?').data
id = item['id']
questions_category = create_category("Test Ebola Questions").data
regions_category = create_category("Test Regions").data
types_category = create_category("Test Item Types").data
questions_category = create_category(name="Test Ebola Questions").data
regions_category = create_category(name="Test Regions").data
types_category = create_category(name="Test Item Types").data
categorize_item(item, term_for(questions_category, 'Vaccine'))
categorize_item(item, term_for(regions_category, 'Monrovia'))
......
......@@ -16,8 +16,8 @@ from taxonomies.tests.factories import (
)
def create_category(name):
request = APIRequestFactory().put("", {'name': name})
def create_category(**kwargs):
request = APIRequestFactory().put("", kwargs)
view = TaxonomyViewSet.as_view(actions={'put': 'create'})
response = view(request)
......@@ -70,7 +70,7 @@ def test_create_a_category():
old_count = count_taxonomies()
assert not taxonomy_exists(category)
response = create_category(category)
response = create_category(name=category)
assert status.is_success(response.status_code), response.data
new_count = count_taxonomies()
......
......@@ -34,12 +34,12 @@ def get_term_itemcount_response(taxonomy_slug, get_params=None):
@pytest.fixture
def questions_category_slug():
return create_category("Test Ebola Questions").data['slug']
return create_category(name="Test Ebola Questions").data['slug']
@pytest.fixture
def regions_category():
return create_category("Regions").data
return create_category(name="Regions").data
@pytest.mark.django_db
......
......@@ -69,7 +69,7 @@ class ItemViewSet(viewsets.ModelViewSet, BulkDestroyModelMixin):
return items
@detail_route(methods=['post'])
def add_term(self, request, item_pk):
def add_terms(self, request, item_pk):
try:
item = Item.objects.get(pk=item_pk)
except Item.DoesNotExist as e:
......@@ -77,49 +77,30 @@ class ItemViewSet(viewsets.ModelViewSet, BulkDestroyModelMixin):
return Response(data, status=status.HTTP_404_NOT_FOUND)
term_data = request.data
try:
term = Term.objects.by_taxonomy(
taxonomy=term_data['taxonomy'],
name=term_data['name'],
)
except Term.DoesNotExist as e:
data = {'detail': e.message}
return Response(data, status=status.HTTP_400_BAD_REQUEST)
item.apply_term(term)
serializer = ItemSerializer(item)
return Response(serializer.data, status=status.HTTP_200_OK)
@detail_route(methods=['post'])
def add_free_terms(self, request, item_pk):
try:
item = Item.objects.get(pk=item_pk)
except Item.DoesNotExist as e:
data = {'detail': e.message}
return Response(data, status=status.HTTP_404_NOT_FOUND)
term_data = request.data
try:
taxonomy = Taxonomy.objects.get(slug=term_data['taxonomy'])
except Taxonomy.DoesNotExist as e:
data = {'detail': e.message}
return Response(data, status=status.HTTP_400_BAD_REQUEST)
terms = []
for term_name in term_data.getlist('name'):
term, _ = Term.objects.get_or_create(
taxonomy=taxonomy,
name=term_name,
)
try:
term = Term.objects.by_taxonomy(
taxonomy=taxonomy,
name=term_name,
)
except Term.DoesNotExist as e:
data = {'detail': e.message}
return Response(data, status=status.HTTP_400_BAD_REQUEST)
item.apply_term(term)
terms.append(term)
serializer = ItemSerializer(item)
item.apply_terms(terms)
serializer = ItemSerializer(item)
return Response(serializer.data, status=status.HTTP_200_OK)
@detail_route(methods=['post'])
def delete_all_terms(self, request, item_pk):
taxonomy_slug = request.data['taxonomy']
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('taxonomies', '0003_taxonomy_multiplicity'),
]
operations = [
migrations.AddField(
model_name='taxonomy',
name='vocabulary',
field=models.CharField(default=b'closed', max_length=30, choices=[(b'fixed', 'Not modifiable by any user, system only'), (b'closed', 'Only admin users who have permission to define and edit taxonomies'), (b'open', 'Any user who has permission to use taxonomies')]),
),
]
......@@ -31,6 +31,8 @@ class Taxonomy(models.Model):
def __unicode__(self):
return self.name
# To do Categories, you use 'optional' and 'closed',
# to do free tagging use 'multiple' and 'open'
multiplicity = models.CharField(
choices=(
('optional', _('Zero or One')),
......@@ -40,26 +42,25 @@ class Taxonomy(models.Model):
max_length=30,
)
# My thoughts on how this grows...
#
#
# vocabulary = models.CharField(
# ...
# choices=(
# ('fixed', _('Not modifiable by any user, system only')),
# ('closed', _('Only admin users who have permission to define and edit taxonomies')),
# ('open', _('Any user who has permission to use taxonomies')),
# )
# )
vocabulary = models.CharField(
choices=(
('fixed', _('Not modifiable by any user, system only')),
('closed', _('Only admin users who have permission to define and edit taxonomies')),
('open', _('Any user who has permission to use taxonomies')),
),
default='closed',
max_length=30,
)
# To do Categories, you use 'optional' and 'closed',
# to do free tagging use 'multiple' and 'open'
@property
def is_open(self):
return self.vocabulary == 'open'
class TermManager(models.Manager):
def by_taxonomy(self, taxonomy, name):
""" Fetch an existing Term by its name and its
""" Fetch a Term by its name and its
Taxonomy slug which, together should be unique together.
args:
......@@ -72,21 +73,30 @@ class TermManager(models.Manager):
The term object with the given name in the given Taxonomy.
throws:
DoesNotExist if no Term matches the given combination
DoesNotExist if Taxonomy with the given slug does not exist
DoesNotExist if named Term does not exist, unless the Taxonomy
vocabulary is open - in this case the Term will be created
ValueError if taxonomy is not one of the allowed types
"""
if isinstance(taxonomy, basestring):
taxonomy_slug = taxonomy
elif isinstance(taxonomy, Taxonomy):
taxonomy_slug = taxonomy.slug
else:
taxonomy = Taxonomy.objects.get(slug=taxonomy)
elif not isinstance(taxonomy, Taxonomy):
raise ValueError(
"taxonomy must be a Taxonomy instance "
"or a valid taxonomy slug")
return self.select_related('taxonomy').get(
taxonomy__slug=taxonomy_slug,
name=name
)
if taxonomy.is_open:
term, _ = self.select_related('taxonomy').get_or_create(
taxonomy=taxonomy,
name=name
)
else:
term = self.select_related('taxonomy').get(
taxonomy=taxonomy,
name=name
)
return term
class Term(models.Model):
......
......@@ -50,3 +50,24 @@ def test_is_optional_false_for_multiplicity_multiple():
taxonomy = TaxonomyFactory(multiplicity='multiple')
assert not taxonomy.is_optional
@pytest.mark.django_db
def test_is_open_true_for_vocabulary_open():
taxonomy = TaxonomyFactory(vocabulary='open')
assert taxonomy.is_open
@pytest.mark.django_db
def test_is_open_false_for_vocabulary_fixed():
taxonomy = TaxonomyFactory(vocabulary='fixed')
assert not taxonomy.is_open
@pytest.mark.django_db
def test_is_open_false_for_vocabulary_closed():
taxonomy = TaxonomyFactory(vocabulary='closed')
assert not taxonomy.is_open
......@@ -35,3 +35,29 @@ def test_term_by_taxonomy_with_taxonomies_with_taxonomy(term_with_context):
assert term.name == term_with_context.name
assert term.taxonomy == term_with_context.taxonomy
@pytest.mark.django_db
def test_unknown_term_by_taxonomy_creates_term_if_open():
taxonomy = TaxonomyFactory(vocabulary='open')
term = Term.objects.by_taxonomy(
taxonomy=taxonomy,
name="a term that doesn't exist",
)
assert term.name == "a term that doesn't exist"
assert term.taxonomy == taxonomy
@pytest.mark.django_db
def test_unknown_term_by_taxonomy_throws_exception_if_not_open():
taxonomy = TaxonomyFactory(vocabulary='closed')
with pytest.raises(Term.DoesNotExist) as excinfo:
Term.objects.by_taxonomy(
taxonomy=taxonomy,
name="a term that doesn't exist",
)
assert excinfo.value.message == "Term matching query does not exist."
......@@ -111,7 +111,7 @@ def add_term(item_id, taxonomy_slug, name):
For the moment both the taxonomy and term must already exist.
"""
view = get_view({'post': 'add_term'})
view = get_view({'post': 'add_terms'})
term = {'taxonomy': taxonomy_slug, 'name': name}
request = request_factory.post("", term)
......@@ -144,7 +144,7 @@ def add_free_terms(item_id, taxonomy_slug, names):
The taxonomy must already exist. Any terms that do not exist will
be created
"""
view = get_view({'post': 'add_free_terms'})
view = get_view({'post': 'add_terms'})
terms = {'taxonomy': taxonomy_slug, 'name': names}
request = request_factory.post('', terms)
......
......@@ -2,18 +2,25 @@ from __future__ import unicode_literals, absolute_import
import pytest
from taxonomies.tests.factories import TaxonomyFactory
from transport import items
from ..exceptions import TransportException
@pytest.fixture
def taxonomy():
return TaxonomyFactory(
name="Test Tags", multiplicity='multiple', vocabulary='open')
@pytest.mark.django_db
def test_multiple_new_terms_applied_to_item():
def test_multiple_new_terms_applied_to_item(taxonomy):
item = items.create({'body': "What is the cuse of ebola?"})
term_names = ['Monrovia', 'age 40-45', 'pertinent']
item = items.add_free_terms(
item['id'], 'tags', term_names)
item['id'], taxonomy.slug, term_names)
stored_names = [t['name'] for t in item['terms']]
......@@ -21,12 +28,12 @@ def test_multiple_new_terms_applied_to_item():
@pytest.mark.django_db
def test_add_free_terms_raises_transport_exception_if_item_absent():
def test_add_free_terms_raises_transport_exception_if_item_absent(taxonomy):
unknown_item_id = 6
term_names = ['Monrovia', 'age 40-45', 'pertinent']
with pytest.raises(TransportException) as excinfo:
items.add_free_terms(unknown_item_id, 'tags', term_names)
items.add_free_terms(unknown_item_id, taxonomy.slug, term_names)
error = excinfo.value.message
......@@ -34,7 +41,7 @@ def test_add_free_terms_raises_transport_exception_if_item_absent():
assert error['detail'] == "Message matching query does not exist."
assert error['item_id'] == unknown_item_id
assert error['terms']['name'] == term_names
assert error['terms']['taxonomy'] == 'tags'
assert error['terms']['taxonomy'] == taxonomy.slug
@pytest.mark.django_db
......
......@@ -2,12 +2,19 @@ from __future__ import unicode_literals, absolute_import
import pytest
from data_layer.models import Item
from taxonomies.tests.factories import TermFactory
from taxonomies.tests.factories import (
TaxonomyFactory,
TermFactory
)
from transport import items
from ..exceptions import TransportException
@pytest.fixture
def taxonomy():
return TaxonomyFactory()
@pytest.fixture
def item_data():
item = {'body': "What is the cuse of ebola?"}
......@@ -28,7 +35,7 @@ def test_terms_can_be_added_to_item(item_data):
@pytest.mark.django_db
def test_add_term_fails_if_term_does_not_exist(item_data):
def test_add_term_fails_if_taxonomy_does_not_exist(item_data):
with pytest.raises(TransportException) as excinfo:
items.add_term(
item_data['id'],
......@@ -38,6 +45,22 @@ def test_add_term_fails_if_term_does_not_exist(item_data):
error = excinfo.value.message
assert error['status_code'] == 400
assert error['detail'] == "Taxonomy matching query does not exist."
assert error['term']['name'] == "unknown term name"
@pytest.mark.django_db
def test_add_term_fails_if_term_does_not_exist(taxonomy, item_data):
with pytest.raises(TransportException) as excinfo:
items.add_term(
item_data['id'],
taxonomy.slug,
"unknown term name",
)
error = excinfo.value.message
assert error['status_code'] == 400
assert error['detail'] == "Term matching query does not exist."
assert error['term']['name'] == "unknown term name"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment