From ab4da08d6ebd2dbdc6e2eaf8f3201c3943c99d29 Mon Sep 17 00:00:00 2001 From: Vicky Leong Date: Mon, 6 Jun 2016 11:05:05 -0400 Subject: [PATCH 1/5] Updates to Questionnaire API endpoint (#607) * Update the response for /api/questionnaire Language direction is now indicated with "ltr" or "rtl" as opposed to "l" or "r". This follows the pattern in langnames.json. Give additional field of "language_data", which tells which question maps to which property on a language. * Fix flake8 error(s) * Change questionnaire API view to class-based; Write unit-tests The receiving endpoint now supports raw JSON data, instead of just POST form-data. Resolves #603 --- td/api/urls.py | 4 ++-- td/api/views.py | 42 ++++++++++++++++++++++++++------------ td/resources/models.py | 13 ++++++++++++ td/tests/api/test_views.py | 15 +++++++------- td/tests/test_views.py | 19 +++++++++++++++-- td/views.py | 4 ++-- 6 files changed, 71 insertions(+), 26 deletions(-) diff --git a/td/api/urls.py b/td/api/urls.py index ffd94350..256aa56e 100644 --- a/td/api/urls.py +++ b/td/api/urls.py @@ -1,10 +1,10 @@ from django.conf.urls import url -from .views import questionnaire_json, templanguages_json, lang_assignment_json, lang_assignment_changed_json +from .views import QuestionnaireView, templanguages_json, lang_assignment_json, lang_assignment_changed_json urlpatterns = [ - url(r"^questionnaire/$", questionnaire_json, name="questionnaire"), + url(r"^questionnaire/$", QuestionnaireView.as_view(), name="questionnaire"), url(r"^templanguages/$", templanguages_json, name="templanguages"), url(r"^templanguages/assignment/$", lang_assignment_json, name="templanguages_assignment"), url(r"^templanguages/assignment/changed/$", lang_assignment_changed_json, name="templanguages_changed"), diff --git a/td/api/views.py b/td/api/views.py index 19506940..a7eef083 100644 --- a/td/api/views.py +++ b/td/api/views.py @@ -2,16 +2,21 @@ from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt +from django.views.generic import View from td.models import TempLanguage, Country from td.resources.models import Questionnaire -# I admit this is not the best solution nor a good practice. Our goal is to use the django REST framework to receive -# temporary language submission in the future. -@csrf_exempt -def questionnaire_json(request): - if request.method == "GET": +class QuestionnaireView(View): + + # I admit this is not the best solution nor a good practice. Our goal is to use the django REST framework to receive + # temporary language submission in the future. + @csrf_exempt + def dispatch(self, request, *args, **kwargs): + return super(QuestionnaireView, self).dispatch(request, *args, **kwargs) + + def get(self, request, *args, **kwargs): # In the future, when we're ready to accommodate translations of the questionnaires, we should iterate through # the queryset and construct the data content appropriately. questionnaire = Questionnaire.objects.latest("created_at") @@ -19,36 +24,47 @@ def questionnaire_json(request): "languages": [ { "name": questionnaire.language.ln, - "dir": questionnaire.language.direction, + "dir": questionnaire.language.get_direction_display(), "slug": questionnaire.language.lc, "questionnaire_id": questionnaire.id, + "language_data": questionnaire.language_data, "questions": questionnaire.questions, } ] } return JsonResponse(data, safe=False) - elif request.method == "POST": + + @csrf_exempt + def post(self, request, *args, **kwargs): # First pass only. Will need more validation and refactoring try: message = "" - data = request.POST + data = request.POST if len(request.POST) else json.loads(request.body) questionnaire = Questionnaire.objects.get(pk=data.get("questionnaire_id")) field_mapping = questionnaire.field_mapping + answers = json.loads(data.get("answers")) if len(request.POST) else data.get("answers") obj = TempLanguage(code=data.get("temp_code"), questionnaire=questionnaire, app=data.get("app"), request_id=data.get("request_id"), requester=data.get("requester"), - answers=json.loads(data.get("answers"))) + answers=answers) - for a in json.loads(data.get("answers")): - qid = a.get("question_id") + for answer in answers: + qid = answer.get("question_id") if qid is not None and qid in field_mapping: + answer_text = answer.get("text") if field_mapping[qid] == "country": - obj.country = Country.objects.get(name__iexact=a["text"]) + obj.country = Country.objects.get(name__iexact=answer_text) + elif field_mapping[qid] == "direction": + obj.direction = "l" if answer_text.lower() == "yes" else "r" else: - obj.__dict__[field_mapping[qid]] = a["text"] + obj.__dict__[field_mapping[qid]] = answer_text obj.save() + except Questionnaire.DoesNotExist: + message = "questionnaire_id given does not return a matching Questionnaire object" + except Country.DoesNotExist: + message = "The answer for country results in an invalid lookup" except Exception as e: message = e.message diff --git a/td/resources/models.py b/td/resources/models.py index 11e8a857..27f69ff9 100644 --- a/td/resources/models.py +++ b/td/resources/models.py @@ -6,6 +6,15 @@ from td.models import Language +NAME_TO_PROPERTY = { + "name": "ln", + "code": "lc", + "direction": "ld", + "country": "cc", + "region": "lr" +} + + def transform_country_data(data): tree = {"name": "World", "parent": None, "children": []} for code in data: @@ -123,6 +132,10 @@ class Questionnaire(models.Model): def __str__(self): return str(self.pk) + @property + def language_data(self): + return {NAME_TO_PROPERTY.get(field_name): int(qid) for qid, field_name in self.field_mapping.items()} + @property def grouped_questions(self): group = [] diff --git a/td/tests/api/test_views.py b/td/tests/api/test_views.py index e5c97799..8c723b72 100644 --- a/td/tests/api/test_views.py +++ b/td/tests/api/test_views.py @@ -5,7 +5,7 @@ from django.test import TestCase from djcelery.tests.req import RequestFactory -from td.api.views import questionnaire_json, templanguages_json, lang_assignment_json, lang_assignment_changed_json +from td.api.views import QuestionnaireView, templanguages_json, lang_assignment_json, lang_assignment_changed_json from td.models import Language, TempLanguage, Country from td.resources.models import Questionnaire @@ -38,13 +38,14 @@ def setUp(self): field_mapping = {"0": "name", "1": "country"} self.questionnaire = Questionnaire.objects.create(pk=999, language=lang, questions=json.dumps(questions), field_mapping=field_mapping) + self.view = QuestionnaireView() def test_get(self): """ Request should successfully return a JsonResponse """ request = RequestFactory().get(reverse("api:questionnaire")) - response = questionnaire_json(request) + response = self.view.get(request) self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"], "application/json") @@ -61,7 +62,7 @@ def test_post_success(self): "answers": json.dumps([{"question_id": "0", "text": "answer"}, {"question_id": "1", "text": "narnia"}]) } request = RequestFactory().post(reverse("api:questionnaire"), data=data) - response = questionnaire_json(request) + response = self.view.post(request) self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"], "application/json") content = json.loads(response.content) @@ -83,10 +84,10 @@ def test_post_error_country(self): "answers": json.dumps([{"question_id": "0", "text": "answer"}, {"question_id": "1", "text": "oz"}]) } request = RequestFactory().post(reverse("api:questionnaire"), data=data) - content = json.loads(questionnaire_json(request).content) + content = json.loads(self.view.post(request).content) self.assertEqual(content["status"], "error") self.assertGreater(len(content["message"]), 0) - self.assertIn("Country", content["message"]) + self.assertIn("country", content["message"].lower()) def test_post_error_questionnaire(self): """ @@ -102,7 +103,7 @@ def test_post_error_questionnaire(self): "answers": json.dumps([{"question_id": "0", "text": "answer"}, {"question_id": "1", "text": "narnia"}]) } request = RequestFactory().post(reverse("api:questionnaire"), data=data) - content = json.loads(questionnaire_json(request).content) + content = json.loads(self.view.post(request).content) self.assertEqual(content["status"], "error") self.assertGreater(len(content["message"]), 0) self.assertIn("Questionnaire", content["message"]) @@ -120,7 +121,7 @@ def test_post_integrity_error(self): "answers": json.dumps([{"question_id": "0", "text": "answer"}, {"question_id": "1", "text": "narnia"}]) } request = RequestFactory().post(reverse("api:questionnaire"), data=data) - content = json.loads(questionnaire_json(request).content) + content = json.loads(self.view.post(request).content) self.assertEqual(content["status"], "error") self.assertGreater(len(content["message"]), 0) self.assertIn("app", content["message"]) diff --git a/td/tests/test_views.py b/td/tests/test_views.py index c17fba0d..d612e8a3 100644 --- a/td/tests/test_views.py +++ b/td/tests/test_views.py @@ -10,10 +10,10 @@ from djcelery.tests.req import RequestFactory -from td.models import TempLanguage, Language +from td.models import TempLanguage, Language, WARegion from td.resources.models import Questionnaire from td.views import TempLanguageListView, TempLanguageDetailView, TempLanguageUpdateView, AjaxTemporaryCode,\ - TempLanguageAdminView, TempLanguageWizardView, LanguageDetailView + TempLanguageAdminView, TempLanguageWizardView, LanguageDetailView, WARegionDetailView from td.forms import TempLanguageForm @@ -38,6 +38,21 @@ def create_user(): ) +class WARegionDetailViewTestCase(TestCase): + + def setUp(self): + self.object = WARegion.objects.create(name="Middle Earth", slug="middleearth") + + # @patch("td.views.WARegionDetailView.get_object") + def test_get_context_data(self): + view = setup_view(WARegionDetailView()) + view.object = self.object + view.get_object = Mock(return_value=self.object) + context = view.get_context_data() + self.assertIn("gl_directors", context) + self.assertIn("gl_helpers", context) + + class LanguageDetailViewTestCase(TestCase): def setUp(self): self.request = RequestFactory().get("uw/languages/") diff --git a/td/views.py b/td/views.py index ee62a70d..951b03cd 100644 --- a/td/views.py +++ b/td/views.py @@ -572,11 +572,9 @@ def form_valid(self, form): "success": True, "object": self.object, } - print "- self.object is ", self.object temp_lang = self.object.templanguage temp_lang.status = "a" temp_lang.save() - print "- templang.lang_assigned is ", temp_lang.lang_assigned return render(self.request, "resources/language_modal_form.html", context) @@ -719,6 +717,8 @@ def done(self, form_list, **kwargs): try: if field_mapping[qid] == "country": obj.country = Country.objects.get(name__iexact=a["text"]) + elif field_mapping[qid] == "direction": + obj.direction = "l" if a["text"].lower() == "yes" else "r" else: obj.__dict__[field_mapping[qid]] = a["text"] except Country.DoesNotExist: From 74e4e7165ccf8536086333b35895aa5019e7390b Mon Sep 17 00:00:00 2001 From: Vicky Leong Date: Wed, 8 Jun 2016 14:33:51 -0400 Subject: [PATCH 2/5] Fix language assignment when posting with raw (#622) --- td/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/td/api/views.py b/td/api/views.py index a7eef083..9f9d9449 100644 --- a/td/api/views.py +++ b/td/api/views.py @@ -49,7 +49,7 @@ def post(self, request, *args, **kwargs): answers=answers) for answer in answers: - qid = answer.get("question_id") + qid = str(answer.get("question_id")) if qid is not None and qid in field_mapping: answer_text = answer.get("text") if field_mapping[qid] == "country": From 82dbe7acf09a2661f40eb899af5c9327d647a8db Mon Sep 17 00:00:00 2001 From: vleong2332 Date: Wed, 8 Jun 2016 15:55:10 -0400 Subject: [PATCH 3/5] Add logger in QuestionnaireView and debug info in response --- td/api/views.py | 34 +++++++++++++++++++++++++++++++++- td/settings_gondor.py | 2 +- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/td/api/views.py b/td/api/views.py index 9f9d9449..47fe13d0 100644 --- a/td/api/views.py +++ b/td/api/views.py @@ -1,4 +1,5 @@ import json +import logging from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt @@ -8,6 +9,9 @@ from td.resources.models import Questionnaire +logger = logging.getLogger(__name__) + + class QuestionnaireView(View): # I admit this is not the best solution nor a good practice. Our goal is to use the django REST framework to receive @@ -40,24 +44,43 @@ def post(self, request, *args, **kwargs): try: message = "" data = request.POST if len(request.POST) else json.loads(request.body) + logger.warning("DATA: %s", data) questionnaire = Questionnaire.objects.get(pk=data.get("questionnaire_id")) field_mapping = questionnaire.field_mapping + logger.warning("FIELD MAPPING: %s", field_mapping) answers = json.loads(data.get("answers")) if len(request.POST) else data.get("answers") + logger.warning("ANSWERS: %s", answers) obj = TempLanguage(code=data.get("temp_code"), questionnaire=questionnaire, app=data.get("app"), request_id=data.get("request_id"), requester=data.get("requester"), answers=answers) + answer_list = [] + answer_text_list = [] + obj_list = [] + for answer in answers: + answer_list.append(answer) + logger.warning("PROCESSING ANSWER: %s", answer) qid = str(answer.get("question_id")) + logger.warning("QID: %s", qid) if qid is not None and qid in field_mapping: + logger.warning("QID IS IN FIELD MAPPING") answer_text = answer.get("text") + answer_text_list.append(answer_text) + logger.warning("ANSWER TEXT: %s", answer_text) if field_mapping[qid] == "country": obj.country = Country.objects.get(name__iexact=answer_text) + obj_list.append(obj.country) + logger.warning("OBJ.COUNTRY: %s", obj.country) elif field_mapping[qid] == "direction": obj.direction = "l" if answer_text.lower() == "yes" else "r" + obj_list.append(obj.direction) + logger.warning("OBJ.DIRECTION: %s", obj.direction) else: obj.__dict__[field_mapping[qid]] = answer_text + obj_list.append(obj.__dict__[field_mapping[qid]]) + logger.warning("OBJ.whatever: %s", obj.__dict__[field_mapping[qid]]) obj.save() @@ -68,7 +91,16 @@ def post(self, request, *args, **kwargs): except Exception as e: message = e.message - return JsonResponse({"status": "error" if message else "success", "message": message or "Request submitted"}) + return JsonResponse( + { + "status": "error" if message else "success", + "message": message or "Request submitted", + "debug": { + "data": data, + "answers": answers, + } + } + ) def templanguages_json(request): diff --git a/td/settings_gondor.py b/td/settings_gondor.py index 1fb31b3a..60b85ca4 100644 --- a/td/settings_gondor.py +++ b/td/settings_gondor.py @@ -6,7 +6,7 @@ from .settings import * # noqa -DEBUG = False +DEBUG = True TEMPLATE_DEBUG = DEBUG SITE_ID = int(os.environ.get("SITE_ID", "2")) From 69c72067f266f3fdaa70ed846dae2eebe9f4cfb1 Mon Sep 17 00:00:00 2001 From: vleong2332 Date: Thu, 9 Jun 2016 09:32:08 -0400 Subject: [PATCH 4/5] Fix unboundlocal error in debugging QuestionnaireView --- td/api/views.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/td/api/views.py b/td/api/views.py index 47fe13d0..2f92a855 100644 --- a/td/api/views.py +++ b/td/api/views.py @@ -41,6 +41,11 @@ def get(self, request, *args, **kwargs): @csrf_exempt def post(self, request, *args, **kwargs): # First pass only. Will need more validation and refactoring + data = list() + answers = list() + answer_list = list() + answer_text_list = list() + obj_list = list() try: message = "" data = request.POST if len(request.POST) else json.loads(request.body) @@ -55,10 +60,6 @@ def post(self, request, *args, **kwargs): request_id=data.get("request_id"), requester=data.get("requester"), answers=answers) - answer_list = [] - answer_text_list = [] - obj_list = [] - for answer in answers: answer_list.append(answer) logger.warning("PROCESSING ANSWER: %s", answer) @@ -96,8 +97,11 @@ def post(self, request, *args, **kwargs): "status": "error" if message else "success", "message": message or "Request submitted", "debug": { - "data": data, - "answers": answers, + "data": data or "no data", + "answers": answers or "no answers", + "answer_list": answer_list or "no answer_list", + "answer_text_list": answer_text_list or "no answer_text_list", + "obj_list": obj_list or "no obj_list" } } ) From d4507b46105e4d7a246d56f2a3ed636d5912329f Mon Sep 17 00:00:00 2001 From: vleong2332 Date: Thu, 9 Jun 2016 09:48:45 -0400 Subject: [PATCH 5/5] Fix TypeError in debugging QuestionnaireView --- td/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/td/api/views.py b/td/api/views.py index 2f92a855..efe3bfe9 100644 --- a/td/api/views.py +++ b/td/api/views.py @@ -72,7 +72,7 @@ def post(self, request, *args, **kwargs): logger.warning("ANSWER TEXT: %s", answer_text) if field_mapping[qid] == "country": obj.country = Country.objects.get(name__iexact=answer_text) - obj_list.append(obj.country) + obj_list.append(obj.country and obj.country.name) logger.warning("OBJ.COUNTRY: %s", obj.country) elif field_mapping[qid] == "direction": obj.direction = "l" if answer_text.lower() == "yes" else "r"