From c7fe28e9dc065e8f53a41de62ac0414de05f9872 Mon Sep 17 00:00:00 2001 From: Gabriel Augendre Date: Sat, 13 Feb 2021 16:47:34 +0100 Subject: [PATCH] Add API endpoint to create message --- poetry.lock | 27 ++++++++-- pyproject.toml | 12 +++++ .../migrations/0006_auto_20210213_1645.py | 26 ++++++++++ src/pictures/models.py | 6 +-- src/pictures/tests.py | 3 -- src/pictures/tests/__init__.py | 0 src/pictures/tests/conftest.py | 12 +++++ src/pictures/tests/test_api.py | 52 +++++++++++++++++++ src/pictures/urls.py | 8 ++- src/pictures/views.py | 36 +++++++++++-- 10 files changed, 167 insertions(+), 15 deletions(-) create mode 100644 src/pictures/migrations/0006_auto_20210213_1645.py delete mode 100644 src/pictures/tests.py create mode 100644 src/pictures/tests/__init__.py create mode 100644 src/pictures/tests/conftest.py create mode 100644 src/pictures/tests/test_api.py diff --git a/poetry.lock b/poetry.lock index 305ecab..30a98d4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -98,7 +98,7 @@ bcrypt = ["bcrypt"] [[package]] name = "django-anymail" -version = "8.1" +version = "8.2" description = "Django email integration for Amazon SES, Mailgun, Mailjet, Postmark, SendGrid, SendinBlue, SparkPost and other transactional ESPs" category = "main" optional = false @@ -276,6 +276,21 @@ toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "pytest-django" +version = "4.1.0" +description = "A Django plugin for pytest." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +pytest = ">=5.4.0" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +testing = ["django", "django-configurations (>=2.0)"] + [[package]] name = "pytz" version = "2020.5" @@ -388,7 +403,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "f4481753176cb89044373243f6f4432b96b2c850482e4a5b5d6d1f9c27df24ad" +content-hash = "34b835304e7e7c52c384ada11f110aec4bf776f4a568f7f7fc0361a83989a614" [metadata.files] appdirs = [ @@ -432,8 +447,8 @@ django = [ {file = "Django-3.1.5.tar.gz", hash = "sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7"}, ] django-anymail = [ - {file = "django-anymail-8.1.tar.gz", hash = "sha256:0c3e56a339a37e654b7511572564fe0949f4fbb12c072761c9e35cfc49cb4dc1"}, - {file = "django_anymail-8.1-py3-none-any.whl", hash = "sha256:0301f2ea1dde7840e5276a5e2d1ca2a56fd558e2b71800e89ca895c18aa3c615"}, + {file = "django-anymail-8.2.tar.gz", hash = "sha256:6381e04c41b2644e2d3ba2f95ee61ee3ee40cb6184506c52a363b9ddef0b098e"}, + {file = "django_anymail-8.2-py3-none-any.whl", hash = "sha256:e011c582e771ce3970480c10d1e129ac036ba773e37ec56780a79776534b2ba6"}, ] django-cleanup = [ {file = "django-cleanup-5.1.0.tar.gz", hash = "sha256:8976aec12a22913afb3d1fcb541b1aedde2f5ec243e4260c5ff78bb6aa75a089"}, @@ -495,6 +510,10 @@ pytest = [ {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, ] +pytest-django = [ + {file = "pytest-django-4.1.0.tar.gz", hash = "sha256:26f02c16d36fd4c8672390deebe3413678d89f30720c16efb8b2a6bf63b9041f"}, + {file = "pytest_django-4.1.0-py3-none-any.whl", hash = "sha256:10e384e6b8912ded92db64c58be8139d9ae23fb8361e5fc139d8e4f8fc601bc2"}, +] pytz = [ {file = "pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4"}, {file = "pytz-2020.5.tar.gz", hash = "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"}, diff --git a/pyproject.toml b/pyproject.toml index da78f28..1389222 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,19 @@ pytest = "^6.2" pre-commit = "^2.9.3" importlib-metadata = "^3.4.0" typing-extensions = "^3.7.4" +pytest-django = "^4.1.0" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.black] +target-version = ['py38'] + +[tool.isort] +profile = "black" + +[tool.pytest.ini_options] +addopts = "--color=yes" +minversion = "6.0" +DJANGO_SETTINGS_MODULE = "picture_display.settings" diff --git a/src/pictures/migrations/0006_auto_20210213_1645.py b/src/pictures/migrations/0006_auto_20210213_1645.py new file mode 100644 index 0000000..d9c9433 --- /dev/null +++ b/src/pictures/migrations/0006_auto_20210213_1645.py @@ -0,0 +1,26 @@ +# Generated by Django 3.1.5 on 2021-02-13 16:45 + +import phonenumber_field.modelfields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pictures", "0005_auto_20210124_1644"), + ] + + operations = [ + migrations.AlterField( + model_name="contact", + name="display_name", + field=models.CharField(blank=True, max_length=250), + ), + migrations.AlterField( + model_name="contact", + name="phone_number", + field=phonenumber_field.modelfields.PhoneNumberField( + max_length=128, region=None, unique=True + ), + ), + ] diff --git a/src/pictures/models.py b/src/pictures/models.py index 129fcde..5d22c95 100644 --- a/src/pictures/models.py +++ b/src/pictures/models.py @@ -12,11 +12,11 @@ class User(AbstractUser): class Contact(models.Model): - phone_number = PhoneNumberField() - display_name = models.CharField(max_length=250) + phone_number = PhoneNumberField(unique=True) + display_name = models.CharField(max_length=250, blank=True) def __str__(self): - return self.display_name + return self.display_name or self.phone_number class Message(models.Model): diff --git a/src/pictures/tests.py b/src/pictures/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/src/pictures/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/src/pictures/tests/__init__.py b/src/pictures/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pictures/tests/conftest.py b/src/pictures/tests/conftest.py new file mode 100644 index 0000000..6471fb6 --- /dev/null +++ b/src/pictures/tests/conftest.py @@ -0,0 +1,12 @@ +import pytest + +from pictures.models import Contact + + +@pytest.fixture +@pytest.mark.django_db +def contact(): + return Contact.objects.create( + phone_number="+33611111111", + display_name="Test contact", + ) diff --git a/src/pictures/tests/test_api.py b/src/pictures/tests/test_api.py new file mode 100644 index 0000000..09824dc --- /dev/null +++ b/src/pictures/tests/test_api.py @@ -0,0 +1,52 @@ +import json + +import pytest +from django.test import Client +from django.urls import reverse + +from pictures.models import Contact, Message +from pictures.views import STATUS_OK + + +@pytest.mark.django_db +@pytest.mark.parametrize("content", ["This is a test message", ""]) +def test_post_message_existing_contact(client: Client, contact: Contact, content: str): + assert Message.objects.count() == 0 + res = _post_message(client, str(contact.phone_number), content) + assert res.status_code == 201 + data = res.json() + message = Message.objects.first() + assert data["status"] == STATUS_OK + assert data["data"]["id"] == message.pk + assert message.sender == contact + assert message.content == content + + +def _post_message(client: Client, phone_number: str, content: str): + res = client.post( + reverse("api-create-message"), + data=json.dumps( + { + "phone_number": phone_number, + "content": content, + } + ), + content_type="application/json", + ) + return res + + +@pytest.mark.django_db +@pytest.mark.parametrize("content", ["This is a test message", ""]) +def test_post_message_new_contact(client: Client, content: str): + assert Message.objects.count() == 0 + phone_number = "+33622222222" + res = _post_message(client, phone_number, content) + data = res.json() + assert res.status_code == 201 + message = Message.objects.first() + assert data["status"] == STATUS_OK + assert data["data"]["id"] == message.pk + assert message.sender.phone_number == phone_number + assert message.sender.display_name == "" + assert message.content == content diff --git a/src/pictures/urls.py b/src/pictures/urls.py index 5937c25..9ee37c2 100644 --- a/src/pictures/urls.py +++ b/src/pictures/urls.py @@ -2,10 +2,16 @@ from django.conf import settings from django.conf.urls.static import static from django.urls import path -from pictures.views import MessageDetailView, MessageListView, send_email +from pictures.views import ( + MessageDetailView, + MessageListView, + create_message, + send_email, +) urlpatterns = [ path("", MessageListView.as_view(), name="messages-list"), path("/", MessageDetailView.as_view(), name="messages-detail"), path("/email/", send_email, name="messages-to-email"), + path("api/message/", create_message, name="api-create-message"), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/src/pictures/views.py b/src/pictures/views.py index 8883672..d457827 100644 --- a/src/pictures/views.py +++ b/src/pictures/views.py @@ -1,3 +1,4 @@ +import json from smtplib import SMTPException from anymail.exceptions import AnymailRequestsAPIError @@ -7,9 +8,13 @@ from django.http import JsonResponse from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from django.views import generic -from django.views.decorators.csrf import ensure_csrf_cookie +from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie +from django.views.decorators.http import require_http_methods -from pictures.models import Message +from pictures.models import Contact, Message + +STATUS_OK = "ok" +STATUS_ERROR = "error" class MessageListView(generic.ListView): @@ -49,5 +54,28 @@ def send_email(request, pk): email.attach_file(media.file.path) email.send(fail_silently=False) except (SMTPException, AnymailRequestsAPIError) as e: - return JsonResponse({"status": "error", "message": str(e)}, status=500) - return JsonResponse({"status": "ok"}, status=200) + return _error_response(str(e)) + return JsonResponse({"status": STATUS_OK}, status=200) + + +def _error_response(message: str, status_code: int = 500): + return JsonResponse( + {"status": STATUS_ERROR, "message": message}, status=status_code + ) + + +@csrf_exempt +@require_http_methods(["POST"]) +def create_message(request): + try: + body = request.body.decode("utf-8") + data = json.loads(body) + phone_number = data.get("phone_number") + if phone_number is None: + return _error_response("phone_number is required", 400) + contact, _ = Contact.objects.get_or_create(phone_number=phone_number) + content = data.get("content", "") + message = Message.objects.create(sender=contact, content=content) + except Exception as e: + return _error_response(str(e)) + return JsonResponse({"status": STATUS_OK, "data": {"id": message.pk}}, status=201)