...
 
Commits (12)
......@@ -18,3 +18,4 @@ TAGS
*.~lock.*#
.python-version
.ve
.ignore
......@@ -50,5 +50,4 @@ pytest-env = "*"
pytest-pythonpath = "*"
werkzeug = "*"
pydocstyle = "*"
django-dynamic-fixture = "*"
safety = "*"
This diff is collapsed.
......@@ -3,6 +3,7 @@ from django.db import models
from django.dispatch.dispatcher import receiver
from django.utils.translation import ugettext_lazy as _
from constance.backends import Backend
from constance.backends.database import DatabaseBackend
from taxonomies.exceptions import TermException
......@@ -172,3 +173,21 @@ class CustomConstanceBackend(DatabaseBackend):
def __init__(self, *args, **kwargs):
super(CustomConstanceBackend, self).__init__(*args, **kwargs)
self._model = CustomConstance
class CustomConstanceTestBackend(Backend):
"""
If we use the database backend, constants are initialised by pytest's test
discovery before the test database has been created, so tests can fail
depending on the contents of the actual non-test database.
"""
def __init__(self, *args, **kwargs):
self.store = {}
def get(self, key):
return self.store.get(key, None)
def set(self, key, value):
self.store[key] = value
# Might need to implement mget at a future date
......@@ -58,6 +58,7 @@ class AddEditItemForm(forms.Form):
def __init__(self, *args, **kwargs):
feedback_disabled = kwargs.pop('feedback_disabled', False)
form_disabled = kwargs.pop('form_disabled', False)
keyvalues = kwargs.pop('keyvalues', False)
super(AddEditItemForm, self).__init__(*args, **kwargs)
......@@ -72,6 +73,9 @@ class AddEditItemForm(forms.Form):
self._maybe_add_feedback_type_field()
self._maybe_add_age_range_field()
if keyvalues:
self._add_key_value_fields(keyvalues)
def _maybe_add_category_field(self):
# This used to be more flexible in that we had partial
# support for per- item/feedback/message type categories
......@@ -143,3 +147,15 @@ class AddEditItemForm(forms.Form):
return choices
return None
def _add_key_value_fields(self, keyvalues):
for key, value in keyvalues.items():
field_name = f'item-keyvalue-{key}'
self.fields[field_name] = forms.CharField(
required=False, label=key
)
def get_key_value_fields(self):
for field_name in self.fields:
if field_name.startswith('item-keyvalue-'):
yield self[field_name]
......@@ -207,16 +207,12 @@
</div>
{% endif %}
{% if keyvalues %}
{% for kv in keyvalues.items %}
<div class="item-keyvalue item-keyvalue-{{ kv.0 }}">
<label for="item-keyvalue-{{ kv.0 }}">{{ kv.0 }}</label>
<input id="item-keyvalue-{{ kv.0 }}" class="form-control item-keyvalue-value" value="{{ kv.1 }}" disabled/>
</div>
{% endfor %}
{% endif %}
{% for kv_field in form.get_key_value_fields %}
<div class="item-keyvalue {{ kv_field.name }}">
<label for="{{ kv_field.name }}">{{ kv_field.label }}</label>
{{ kv_field|add_class:'form-control item-keyvalue-value' }}
</div>
{% endfor %}
</div>
</div>
......
......@@ -174,3 +174,26 @@ def test_category_field_is_not_required(
form = AddEditItemForm()
assert not form.fields['category'].required
@patch.object(AddEditItemForm, '_get_category_choices')
@patch.object(AddEditItemForm, '_get_feedback_type_choices')
@patch.object(AddEditItemForm, '_get_age_range_choices')
def test_form_has_keyvalue_fields(
age_range_choices,
feedback_choices,
category_choices
):
age_range_choices.return_value = None
feedback_choices.return_value = None
category_choices.return_value = None
form = AddEditItemForm(
keyvalues={
'CONTRIBUTER': 'TWB',
'URL': 'https://twitter.com/WelshDalaiLama/status/1248884952558718976',
}
)
assert 'item-keyvalue-CONTRIBUTER' in form.fields
assert 'item-keyvalue-URL' in form.fields
......@@ -983,6 +983,27 @@ def test_form_initial_values_include_feedback_type(generic_item):
assert initial['feedback_type'] == ['concern']
@pytest.mark.django_db
def test_form_initial_values_include_keyvalues(generic_item):
with patch('hid.views.item.transport.items.get') as get_item:
generic_item['values'] = {
'CONTRIBUTER': 'TWB',
'URL': 'https://twitter.com/WelshDalaiLama/status/1248884952558718976',
}
get_item.return_value = generic_item
(view, response) = make_request(
AddEditItemView,
'edit-item',
kwargs={'item_id': 103}
)
initial = view.get_initial()
assert initial['item-keyvalue-CONTRIBUTER'] == 'TWB'
assert initial['item-keyvalue-URL'] == 'https://twitter.com/WelshDalaiLama/status/1248884952558718976'
@pytest.mark.django_db
def test_feedback_disabled_if_user_does_not_have_permission(generic_item):
with patch('hid.views.item.transport.items.get') as get_item:
......@@ -1023,3 +1044,17 @@ def test_feedback_enabled_if_user_has_permission(generic_item):
form = response.context_data['form']
assert form.fields['body'].disabled is False
@pytest.mark.django_db
def test_item_keyvalues_can_be_updated(view, update_form, item_type_taxonomy):
TaxonomyFactory(name='Age Ranges', slug='age-ranges')
transport.items.add_keyvalue(view.item['id'], 'CONTRIBUTER', 'TWB')
update_form.cleaned_data['item-keyvalue-CONTRIBUTER'] = 'TWB modified'
view.form_valid(update_form)
assert_no_messages(view.request, messages.ERROR)
item = transport.items.get(view.item['id'])
assert item['values']['CONTRIBUTER'] == 'TWB modified'
......@@ -177,6 +177,8 @@ class AddEditItemView(FormView):
)
),
}
for key, value in self.item.get('values', {}).items():
initial[f'item-keyvalue-{key}'] = value
taxonomy = ITEM_TYPE_CATEGORY.get('all')
if (taxonomy and taxonomy in self.item_terms
......@@ -210,6 +212,8 @@ class AddEditItemView(FormView):
kwargs['feedback_disabled'] = self._feedback_disabled()
kwargs['form_disabled'] = self._form_disabled()
if self.item:
kwargs['keyvalues'] = self.item.get('values')
return form_class(**kwargs)
......@@ -242,10 +246,6 @@ class AddEditItemView(FormView):
# Add item and form mode to the context
context['item'] = self.item
# Stashing values in the context because of a naming clash with a method on self.item
# prevent us from referencing values directly in the template
if self.item:
context['keyvalues'] = self.item.get('values')
context['update'] = not (self._form_disabled() or (self.item is None))
......@@ -311,14 +311,17 @@ class AddEditItemView(FormView):
tags = {}
regular_fields = {}
keyvalues = {}
for (field_name, field_value) in data.items():
if field_name in self.tag_fields:
tags[field_name] = field_value
elif field_name.startswith('item-keyvalue-'):
keyvalues[field_name[len('item-keyvalue-'):]] = field_value
else:
regular_fields[field_name] = field_value
return category, tags, feedback_type, age_range, regular_fields
return category, tags, feedback_type, age_range, regular_fields, keyvalues
def _add_tags(self, item_id, tags):
for (taxonomy, value) in tags.items():
......@@ -327,6 +330,10 @@ class AddEditItemView(FormView):
transport.items.add_terms(item_id, taxonomy, term_names)
def _update_keyvalues(self, item_id, keyvalues):
for (key, value) in keyvalues.items():
transport.items.update_keyvalue(item_id, key, value)
def _update_item(self, item_id, form):
""" Update the given item
......@@ -341,7 +348,7 @@ class AddEditItemView(FormView):
"""
(category, tags, feedback_type, age_range,
regular_fields) = self._separate_form_data(form)
regular_fields, keyvalues) = self._separate_form_data(form)
transport.items.update(item_id, regular_fields)
......@@ -366,6 +373,8 @@ class AddEditItemView(FormView):
else:
transport.items.delete_all_terms(item_id, 'age-ranges')
self._update_keyvalues(item_id, keyvalues)
self._add_tags(item_id, tags)
def _create_item(self, form, taxonomy):
......@@ -383,8 +392,10 @@ class AddEditItemView(FormView):
Raises:
TransportException: On API errors
"""
# keyvalues not supported for form creation
(category, tags, feedback_type, age_range,
regular_fields) = self._separate_form_data(form)
regular_fields, _) = self._separate_form_data(form)
if not feedback_type:
feedback_type = [self.item_type[0]['name']]
......
import pytest
from rest_framework import status
from rest_framework.test import APIRequestFactory
from ..views import ItemViewSet
from .item_create_view_tests import create_item
def get_item(id):
view = ItemViewSet.as_view(actions={'get': 'retrieve'})
request = APIRequestFactory().get('')
return view(request, pk=id)
@pytest.mark.django_db
def test_keyvalue_created():
item = create_item(body='Text').data
kwargs = {
'key': 'CONTRIBUTER',
'value': 'TWB',
}
request = APIRequestFactory().post('', kwargs)
view = ItemViewSet.as_view(actions={'post': 'add_keyvalue'})
response = view(request, item_pk=item['id'])
assert status.is_success(response.status_code), response.data
updated_item = get_item(item['id']).data
values = updated_item['values']
assert values['CONTRIBUTER'] == 'TWB'
@pytest.mark.django_db
def test_add_keyvalue_raises_if_item_does_not_exist():
kwargs = {
'key': 'CONTRIBUTER',
'value': 'TWB',
}
request = APIRequestFactory().post('', kwargs)
view = ItemViewSet.as_view(actions={'post': 'add_keyvalue'})
unknown_item_id = 9999
response = view(request, item_pk=unknown_item_id)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.data['detail'] == "Message matching query does not exist."
@pytest.mark.django_db
def test_keyvalue_updated():
item = create_item(body='Text').data
kwargs = {
'key': 'CONTRIBUTER',
'value': 'TWB',
}
request = APIRequestFactory().post('', kwargs)
view = ItemViewSet.as_view(actions={'post': 'add_keyvalue'})
response = view(request, item_pk=item['id'])
assert status.is_success(response.status_code), response.data
kwargs = {
'key': 'CONTRIBUTER',
'value': 'TWB modified',
}
request = APIRequestFactory().post('', kwargs)
view = ItemViewSet.as_view(actions={'post': 'update_keyvalue'})
response = view(request, item_pk=item['id'])
assert status.is_success(response.status_code), response.data
updated_item = get_item(item['id']).data
values = updated_item['values']
assert values['CONTRIBUTER'] == 'TWB modified'
@pytest.mark.django_db
def test_update_keyvalue_raises_if_item_does_not_exist():
kwargs = {
'key': 'CONTRIBUTER',
'value': 'TWB',
}
request = APIRequestFactory().post('', kwargs)
view = ItemViewSet.as_view(actions={'post': 'update_keyvalue'})
unknown_item_id = 9999
response = view(request, item_pk=unknown_item_id)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.data['detail'] == "Message matching query does not exist."
......@@ -193,7 +193,23 @@ class ItemViewSet(viewsets.ModelViewSet, BulkDestroyModelMixin):
keyvalue = request.data
key, _ = Key.objects.get_or_create(key=keyvalue['key'])
value = Value.objects.create(key=key, value=keyvalue['value'], message=item)
Value.objects.create(key=key, value=keyvalue['value'], message=item)
serializer = ItemSerializer(item)
return Response(serializer.data, status=status.HTTP_200_OK)
@action(methods=['post'], detail=True)
def update_keyvalue(self, request, item_pk):
try:
item = Item.objects.get(pk=item_pk)
except Item.DoesNotExist as e:
data = {'detail': str(e)}
return Response(data, status=status.HTTP_404_NOT_FOUND)
keyvalue = request.data
values = Value.objects.filter(key__key=keyvalue['key'], message=item)
values.update(value=keyvalue['value'])
serializer = ItemSerializer(item)
return Response(serializer.data, status=status.HTTP_200_OK)
......
import collections
import json
import re
import sys
import warnings
from copy import deepcopy
from os import path
......@@ -13,6 +14,8 @@ warnings.filterwarnings(
message='Unable to import floppyforms.gis'
)
WE_ARE_TESTING = "pytest" in sys.modules
BASE_DIR = path.abspath(path.dirname(__file__))
DEBUG = False
......@@ -189,7 +192,11 @@ LOGGING = {
# the utf8mb4 encoding/collation whereby the constance model
# must be overriden. We set the max_length to 190 instead of 255.
# See https://github.com/jazzband/django-constance/issues/121
CONSTANCE_BACKEND = 'data_layer.models.CustomConstanceBackend'
if WE_ARE_TESTING:
CONSTANCE_BACKEND = 'data_layer.models.CustomConstanceTestBackend'
else:
CONSTANCE_BACKEND = 'data_layer.models.CustomConstanceBackend'
CONSTANCE_CONFIG = {
'CONTEXT_LOCATION': ("Cox's Bazaar, Bangladesh",
......
......@@ -203,6 +203,23 @@ def add_keyvalue(item_id, key, value):
raise TransportException(response.data)
def update_keyvalue(item_id, key, value):
view = get_view({'post': 'update_keyvalue'})
keyvalue = {'key': key, 'value': value}
request = request_factory.post('', keyvalue)
response = view(request, item_pk=item_id)
if status.is_success(response.status_code):
return response.data
response.data['status_code'] = response.status_code
response.data['value'] = keyvalue
response.data['item_id'] = item_id
raise TransportException(response.data)
def delete_all_terms(item_id, taxonomy_slug):
view = get_view({'post': 'delete_all_terms'})
......
import pytest
from transport import items
from ..exceptions import TransportException
@pytest.fixture
def item_data():
item = {'body': "What is the cuse of ebola?"}
return items.create(item)
@pytest.mark.django_db
def test_keyvalues_can_be_added_to_item(item_data):
item_id = item_data['id']
response = items.add_keyvalue(item_id, 'CONTRIBUTER', 'TWB')
assert response['values'] == {'CONTRIBUTER': 'TWB'}
@pytest.mark.django_db
def test_keyvalues_can_be_updated_on_item(item_data):
item_id = item_data['id']
response = items.add_keyvalue(item_id, 'CONTRIBUTER', 'TWB')
response = items.update_keyvalue(item_id, 'CONTRIBUTER', 'TWB modified')
assert response['values'] == {'CONTRIBUTER': 'TWB modified'}
@pytest.mark.django_db
def test_add_raises_when_item_does_not_exist():
unknown_item_id = 9999
with pytest.raises(TransportException) as excinfo:
items.add_keyvalue(unknown_item_id, 'CONTRIBUTER', 'TWB modified')
assert excinfo.value.message['status_code'] == 404
assert excinfo.value.message['value'] == {
'CONTRIBUTER': 'TWB modified'
}
assert excinfo.value.message['item_id'] == 9999
@pytest.mark.django_db
def test_update_raises_when_item_does_not_exist():
unknown_item_id = 9999
with pytest.raises(TransportException) as excinfo:
items.update_keyvalue(unknown_item_id, 'CONTRIBUTER', 'TWB modified')
assert excinfo.value.message['status_code'] == 404
assert excinfo.value.message['value'] == {
'CONTRIBUTER': 'TWB modified'
}
assert excinfo.value.message['item_id'] == 9999
import pytest
from django_dynamic_fixture import G
from data_layer.models import Item
from data_layer.tests.factories import ItemFactory
from transport.items import list_options
@pytest.mark.django_db
def test_list_options_for_gender_retrieved_all_options():
items = G(Item, n=5)
expected_genders = [an_item.gender for an_item in items]
actual_genders = list(list_options('gender'))
assert expected_genders == actual_genders
@pytest.mark.django_db
def test_list_options_for_gender_unique():
G(Item, gender='male', n=2)
G(Item, gender='female', n=2)
G(Item, gender='xie')
expected_genders = ['female', 'male', 'xie']
actual_genders = list(list_options('gender'))
assert expected_genders == actual_genders
ItemFactory(gender='male')
ItemFactory(gender='male')
ItemFactory(gender='female')
ItemFactory(gender='female')
ItemFactory(gender='other')
genders = list(list_options('gender'))
assert genders == ['female', 'male', 'other']
@pytest.mark.django_db
def test_list_options_for_gender_exclude_blank():
G(Item, gender='', n=2)
G(Item, gender='female', n=2)
G(Item, gender='xie')
expected_genders = ['female', 'xie']
actual_genders = list(list_options('gender'))
assert expected_genders == actual_genders
ItemFactory(gender='')
ItemFactory(gender='female')
ItemFactory(gender='xie')
@pytest.mark.django_db
def test_list_options_for_location_retrieved_all_options():
items = G(Item, n=5)
expected_locations = [an_item.location for an_item in items]
actual_locations = list(list_options('location'))
assert expected_locations == actual_locations
genders = list(list_options('gender'))
assert '' not in genders
@pytest.mark.django_db
def test_list_options_for_location_unique():
G(Item, location='Cambridge', n=2)
G(Item, location='Brighton', n=2)
G(Item, location='London')
expected_locations = ['Brighton', 'Cambridge', 'London']
actual_locations = list(list_options('location'))
assert expected_locations == actual_locations
ItemFactory(location='Cambridge')
ItemFactory(location='Cambridge')
ItemFactory(location='Brighton')
ItemFactory(location='Brighton')
ItemFactory(location='London')
@pytest.mark.django_db
def test_list_options_for_location_exclude_blank():
G(Item, location='', n=2)
G(Item, location='Brighton', n=2)
G(Item, location='London')
expected_locations = ['Brighton', 'London']
actual_locations = list(list_options('location'))
assert expected_locations == actual_locations
@pytest.mark.django_db
def test_list_options_for_contributor_retrieved_all_options():
items = G(Item, n=5)
expected_contributors = [an_item.contributor for an_item in items]
actual_contributors = list(list_options('contributor'))
assert expected_contributors == actual_contributors
@pytest.mark.django_db
def test_list_options_for_contributor_unique():
G(Item, contributor='Rojina Akter', n=2)
G(Item, contributor='Nur Ankis', n=2)
G(Item, contributor='Rashada')
expected_contributors = ['Nur Ankis', 'Rashada', 'Rojina Akter', ]
actual_contributors = list(list_options('contributor'))
assert expected_contributors == actual_contributors
@pytest.mark.django_db
def test_list_options_for_contributor_exclude_blank():
items = G(Item, contributor='', n=2)
items.extend(G(Item, contributor='Nur Ankis', n=2))
items.append(G(Item, contributor='Rashada'))
expected_contributors = ['Nur Ankis', 'Rashada']
actual_contributors = list(list_options('contributor'))
assert expected_contributors == actual_contributors
@pytest.mark.django_db
def test_list_options_for_collection_type_retrieved_all_options():
items = G(Item, n=5)
expected_collection_types = [an_item.collection_type for an_item in items]
actual_collection_types = list(list_options('collection_type'))
assert expected_collection_types == actual_collection_types
@pytest.mark.django_db
def test_list_options_for_collection_type_unique():
G(Item, collection_type='Rojina Akter', n=2)
G(Item, collection_type='Nur Ankis', n=2)
G(Item, collection_type='Rashada')
expected_collection_types = ['Nur Ankis', 'Rashada', 'Rojina Akter']
actual_collection_types = list(list_options('collection_type'))
assert expected_collection_types == actual_collection_types
@pytest.mark.django_db
def test_list_options_for_collection_type_exclude_blank():
items = G(Item, collection_type='', n=2)
items.extend(G(Item, collection_type='Nur Ankis', n=2))
items.append(G(Item, collection_type='Rashada'))
expected_collection_types = ['Nur Ankis', 'Rashada']
actual_collection_types = list(list_options('collection_type'))
assert expected_collection_types == actual_collection_types
assert actual_locations == ['Brighton', 'Cambridge', 'London']