From bcb2948417b336d8f788d0a09f762b564070baab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 29 Sep 2014 16:44:14 +0200 Subject: [PATCH 1/2] US #954: Task #1115: Create feedback endpoint --- settings/common.py | 6 +++ taiga/feedback/__init__.py | 17 +++++++ taiga/feedback/api.py | 51 +++++++++++++++++++ taiga/feedback/apps.py | 32 ++++++++++++ taiga/feedback/migrations/0001_initial.py | 29 +++++++++++ taiga/feedback/migrations/__init__.py | 0 taiga/feedback/models.py | 34 +++++++++++++ taiga/feedback/permissions.py | 23 +++++++++ taiga/feedback/routers.py | 22 ++++++++ taiga/feedback/serializers.py | 24 +++++++++ taiga/feedback/services.py | 29 +++++++++++ .../feedback_notification-body-html.jinja | 37 ++++++++++++++ .../feedback_notification-body-text.jinja | 11 ++++ .../feedback_notification-subject.jinja | 1 + taiga/routers.py | 7 +++ .../resources_permissions/test_feedback.py | 27 ++++++++++ tests/integration/test_feedback.py | 47 +++++++++++++++++ 17 files changed, 397 insertions(+) create mode 100644 taiga/feedback/__init__.py create mode 100644 taiga/feedback/api.py create mode 100644 taiga/feedback/apps.py create mode 100644 taiga/feedback/migrations/0001_initial.py create mode 100644 taiga/feedback/migrations/__init__.py create mode 100644 taiga/feedback/models.py create mode 100644 taiga/feedback/permissions.py create mode 100644 taiga/feedback/routers.py create mode 100644 taiga/feedback/serializers.py create mode 100644 taiga/feedback/services.py create mode 100644 taiga/feedback/templates/emails/feedback_notification-body-html.jinja create mode 100644 taiga/feedback/templates/emails/feedback_notification-body-text.jinja create mode 100644 taiga/feedback/templates/emails/feedback_notification-subject.jinja create mode 100644 tests/integration/resources_permissions/test_feedback.py create mode 100644 tests/integration/test_feedback.py diff --git a/settings/common.py b/settings/common.py index 72f2a0cf..d726741f 100644 --- a/settings/common.py +++ b/settings/common.py @@ -193,6 +193,7 @@ INSTALLED_APPS = [ "taiga.timeline", "taiga.mdrender", "taiga.export_import", + "taiga.feedback", "rest_framework", "djmail", @@ -323,6 +324,11 @@ TAGS_PREDEFINED_COLORS = ["#fce94f", "#edd400", "#c4a000", "#8ae234", "#5c3566", "#ef2929", "#cc0000", "#a40000", "#2e3436",] +# Feedback module settings +FEEDBACK_ENABLED = True +FEEDBACK_EMAIL = "support@taiga.io" + + # NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE TEST_RUNNER="django.test.runner.DiscoverRunner" diff --git a/taiga/feedback/__init__.py b/taiga/feedback/__init__.py new file mode 100644 index 00000000..17e45261 --- /dev/null +++ b/taiga/feedback/__init__.py @@ -0,0 +1,17 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +default_app_config = "taiga.feedback.apps.FeedbackAppConfig" diff --git a/taiga/feedback/api.py b/taiga/feedback/api.py new file mode 100644 index 00000000..8476c365 --- /dev/null +++ b/taiga/feedback/api.py @@ -0,0 +1,51 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base import response +from taiga.base.api import viewsets + +from . import permissions +from . import serializers +from . import services + +import copy + + +class FeedbackViewSet(viewsets.ViewSet): + permission_classes = (permissions.FeedbackPermission,) + serializer_class = serializers.FeedbackEntrySerializer + + def create(self, request, **kwargs): + self.check_permissions(request, "create", None) + + data = copy.deepcopy(request.DATA) + data.update({"full_name": request.user.get_full_name(), + "email": request.user.email}) + + serializer = self.serializer_class(data=data) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) + + self.object = serializer.save(force_insert=True) + + extra = { + "HTTP_HOST": request.META.get("HTTP_HOST", None), + "HTTP_REFERER": request.META.get("HTTP_REFERER", None), + "HTTP_USER_AGENT": request.META.get("HTTP_USER_AGENT", None), + } + services.send_feedback(self.object, extra) + + return response.Ok(serializer.data) diff --git a/taiga/feedback/apps.py b/taiga/feedback/apps.py new file mode 100644 index 00000000..7ae2c1af --- /dev/null +++ b/taiga/feedback/apps.py @@ -0,0 +1,32 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.apps import AppConfig +from django.apps import apps +from django.conf import settings +from django.conf.urls import include, url + +from .routers import router + + +class FeedbackAppConfig(AppConfig): + name = "taiga.feedback" + verbose_name = "Feedback" + + def ready(self): + if settings.FEEDBACK_ENABLED: + from taiga.urls import urlpatterns + urlpatterns.append(url(r'^api/v1/', include(router.urls))) diff --git a/taiga/feedback/migrations/0001_initial.py b/taiga/feedback/migrations/0001_initial.py new file mode 100644 index 00000000..118638c5 --- /dev/null +++ b/taiga/feedback/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='FeedbackEntry', + fields=[ + ('id', models.AutoField(primary_key=True, auto_created=True, verbose_name='ID', serialize=False)), + ('full_name', models.CharField(verbose_name='full name', max_length=256)), + ('email', models.EmailField(verbose_name='email address', max_length=255)), + ('comment', models.TextField(verbose_name='comment')), + ('created_date', models.DateTimeField(auto_now_add=True, verbose_name='created date')), + ], + options={ + 'verbose_name': 'feedback entry', + 'verbose_name_plural': 'feedback entries', + 'ordering': ['-created_date', 'id'], + }, + bases=(models.Model,), + ), + ] diff --git a/taiga/feedback/migrations/__init__.py b/taiga/feedback/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/feedback/models.py b/taiga/feedback/models.py new file mode 100644 index 00000000..a56de2b9 --- /dev/null +++ b/taiga/feedback/models.py @@ -0,0 +1,34 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + + +class FeedbackEntry(models.Model): + full_name = models.CharField(null=False, blank=False, max_length=256, + verbose_name=_('full name')) + email = models.EmailField(null=False, blank=False, max_length=255, + verbose_name=_('email address')) + comment = models.TextField(null=False, blank=False, + verbose_name=_("comment")) + created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True, + verbose_name=_("created date")) + + class Meta: + verbose_name = "feedback entry" + verbose_name_plural = "feedback entries" + ordering = ["-created_date", "id"] diff --git a/taiga/feedback/permissions.py b/taiga/feedback/permissions.py new file mode 100644 index 00000000..6b755975 --- /dev/null +++ b/taiga/feedback/permissions.py @@ -0,0 +1,23 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from taiga.base.api.permissions import TaigaResourcePermission +from taiga.base.api.permissions import IsAuthenticated + + +class FeedbackPermission(TaigaResourcePermission): + create_perms = IsAuthenticated() diff --git a/taiga/feedback/routers.py b/taiga/feedback/routers.py new file mode 100644 index 00000000..a3486b52 --- /dev/null +++ b/taiga/feedback/routers.py @@ -0,0 +1,22 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base import routers +from . import api + + +router = routers.DefaultRouter(trailing_slash=False) +router.register(r"feedback", api.FeedbackViewSet, base_name="feedback") diff --git a/taiga/feedback/serializers.py b/taiga/feedback/serializers.py new file mode 100644 index 00000000..f04d5b3e --- /dev/null +++ b/taiga/feedback/serializers.py @@ -0,0 +1,24 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from rest_framework import serializers + +from . import models + + +class FeedbackEntrySerializer(serializers.ModelSerializer): + class Meta: + model = models.FeedbackEntry diff --git a/taiga/feedback/services.py b/taiga/feedback/services.py new file mode 100644 index 00000000..10362208 --- /dev/null +++ b/taiga/feedback/services.py @@ -0,0 +1,29 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.conf import settings + +from djmail.template_mail import MagicMailBuilder + + +def send_feedback(feedback_entry, extra): + support_email = settings.FEEDBACK_EMAIL + + if support_email: + mbuilder = MagicMailBuilder() + email = mbuilder.feedback_notification(support_email, {"feedback_entry": feedback_entry, + "extra": extra}) + email.send() diff --git a/taiga/feedback/templates/emails/feedback_notification-body-html.jinja b/taiga/feedback/templates/emails/feedback_notification-body-html.jinja new file mode 100644 index 00000000..2888f56f --- /dev/null +++ b/taiga/feedback/templates/emails/feedback_notification-body-html.jinja @@ -0,0 +1,37 @@ +{% extends "emails/base.jinja" %} + +{% block body %} + + + + + + + + + + {% if extra %} + + + + +
+ From: + + {{ feedback_entry.full_name }} [{{ feedback_entry.email }}] +
+ Comment: + + {{ feedback_entry.comment|linebreaks }} +
+ Extra: + +
+ {% for k, v in extra.items() %} +
{{ k }}
+
{{ v }}
+ {% endfor %} +
+
+{% endblock %} diff --git a/taiga/feedback/templates/emails/feedback_notification-body-text.jinja b/taiga/feedback/templates/emails/feedback_notification-body-text.jinja new file mode 100644 index 00000000..fd23785b --- /dev/null +++ b/taiga/feedback/templates/emails/feedback_notification-body-text.jinja @@ -0,0 +1,11 @@ +--------- +- From: {{ feedback_entry.full_name }} [{{ feedback_entry.email }}] +--------- +- Comment: +{{ feedback_entry.comment }} +---------{% if extra %} +- Extra: +{% for k, v in extra.items() %} + - {{ k }}: {{ v }} +{% endfor %} +{% endif %}---------- diff --git a/taiga/feedback/templates/emails/feedback_notification-subject.jinja b/taiga/feedback/templates/emails/feedback_notification-subject.jinja new file mode 100644 index 00000000..8f0f4b9c --- /dev/null +++ b/taiga/feedback/templates/emails/feedback_notification-subject.jinja @@ -0,0 +1 @@ +[Taiga] Feedback from {{ feedback_entry.full_name }} <{{ feedback_entry.email }}> diff --git a/taiga/routers.py b/taiga/routers.py index 7807aa73..32bb9001 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -128,4 +128,11 @@ router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links") # Notify policies from taiga.projects.notifications.api import NotifyPolicyViewSet + router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notifications") + + +# feedback +# - see taiga.feedback.routers and taiga.feedback.apps + + diff --git a/tests/integration/resources_permissions/test_feedback.py b/tests/integration/resources_permissions/test_feedback.py new file mode 100644 index 00000000..c3a8d51e --- /dev/null +++ b/tests/integration/resources_permissions/test_feedback.py @@ -0,0 +1,27 @@ +from django.core.urlresolvers import reverse + +from tests import factories as f +from tests.utils import helper_test_http_method + +from taiga.base.utils import json + +import pytest +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + m.user = f.UserFactory.create() + return m + + +def test_feedback_create(client, data): + url = reverse("feedback-list") + users = [None, data.user] + + feedback_data = {"comment": "One feedback comment"} + feedback_data = json.dumps(feedback_data) + + results = helper_test_http_method(client, 'post', url, feedback_data, users) + assert results == [401, 200] diff --git a/tests/integration/test_feedback.py b/tests/integration/test_feedback.py new file mode 100644 index 00000000..478afc8f --- /dev/null +++ b/tests/integration/test_feedback.py @@ -0,0 +1,47 @@ +from django.core.urlresolvers import reverse + +from tests import factories as f +from tests.utils import helper_test_http_method + +from taiga.base.utils import json + +import pytest +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def user(): + return f.UserFactory.create() + + +def test_create_feedback(client, user): + url = reverse("feedback-list") + + feedback_data = {"comment": "One feedback comment"} + feedback_data = json.dumps(feedback_data) + + client.login(user) + + response = client.post(url, feedback_data, content_type="application/json") + assert response.status_code == 200 + + assert response.data.get("id", None) + assert response.data.get("created_date", None) + assert response.data.get("full_name", user.full_name) + assert response.data.get("email", user.email) + + client.logout() + + +def test_create_feedback_without_comments(client, user): + url = reverse("feedback-list") + + feedback_data = json.dumps({}) + + client.login(user) + + response = client.post(url, feedback_data, content_type="application/json") + assert response.status_code == 400 + assert response.data.get("comment", None) + + client.logout() From 32c5a681c7dd498204d38d5d1152aa7f67e09069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 2 Oct 2014 21:25:49 +0200 Subject: [PATCH 2/2] Add feedback entries to the Admin panel --- taiga/feedback/admin.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 taiga/feedback/admin.py diff --git a/taiga/feedback/admin.py b/taiga/feedback/admin.py new file mode 100644 index 00000000..512abb16 --- /dev/null +++ b/taiga/feedback/admin.py @@ -0,0 +1,31 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.contrib import admin + +from . import models + + +class FeedbackEntryAdmin(admin.ModelAdmin): + list_display = ['created_date', 'full_name', 'email' ] + list_display_links = list_display + list_filter = ['created_date',] + date_hierarchy = "created_date" + ordering = ("-created_date", "id") + search_fields = ("full_name", "email", "id") + + +admin.site.register(models.FeedbackEntry, FeedbackEntryAdmin)