parent
b82f6a4743
commit
bcb2948417
|
@ -193,6 +193,7 @@ INSTALLED_APPS = [
|
||||||
"taiga.timeline",
|
"taiga.timeline",
|
||||||
"taiga.mdrender",
|
"taiga.mdrender",
|
||||||
"taiga.export_import",
|
"taiga.export_import",
|
||||||
|
"taiga.feedback",
|
||||||
|
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"djmail",
|
"djmail",
|
||||||
|
@ -323,6 +324,11 @@ TAGS_PREDEFINED_COLORS = ["#fce94f", "#edd400", "#c4a000", "#8ae234",
|
||||||
"#5c3566", "#ef2929", "#cc0000", "#a40000",
|
"#5c3566", "#ef2929", "#cc0000", "#a40000",
|
||||||
"#2e3436",]
|
"#2e3436",]
|
||||||
|
|
||||||
|
# Feedback module settings
|
||||||
|
FEEDBACK_ENABLED = True
|
||||||
|
FEEDBACK_EMAIL = "support@taiga.io"
|
||||||
|
|
||||||
|
|
||||||
# NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE
|
# NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE
|
||||||
TEST_RUNNER="django.test.runner.DiscoverRunner"
|
TEST_RUNNER="django.test.runner.DiscoverRunner"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
default_app_config = "taiga.feedback.apps.FeedbackAppConfig"
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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)
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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)))
|
|
@ -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,),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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"]
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
from taiga.base.api.permissions import TaigaResourcePermission
|
||||||
|
from taiga.base.api.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
|
||||||
|
class FeedbackPermission(TaigaResourcePermission):
|
||||||
|
create_perms = IsAuthenticated()
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.base import routers
|
||||||
|
from . import api
|
||||||
|
|
||||||
|
|
||||||
|
router = routers.DefaultRouter(trailing_slash=False)
|
||||||
|
router.register(r"feedback", api.FeedbackViewSet, base_name="feedback")
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class FeedbackEntrySerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.FeedbackEntry
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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()
|
|
@ -0,0 +1,37 @@
|
||||||
|
{% extends "emails/base.jinja" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<table border="0" width="100%" cellpadding="4" cellspacing="10" class="table-body" style="border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td valign="top" style="border-top: 1px solid gray; border-bottom: 1px solid gray;">
|
||||||
|
<strong>From:</strong>
|
||||||
|
</td>
|
||||||
|
<td style="border-top: 1px solid gray; border-bottom: 1px solid gray;">
|
||||||
|
{{ feedback_entry.full_name }} [{{ feedback_entry.email }}]
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td valign="top" style="border-top: 1px solid gray; border-bottom: 1px solid gray;">
|
||||||
|
<strong>Comment:</strong>
|
||||||
|
</td>
|
||||||
|
<td style="border-top: 1px solid gray; border-bottom: 1px solid gray;">
|
||||||
|
{{ feedback_entry.comment|linebreaks }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if extra %}
|
||||||
|
<tr>
|
||||||
|
<td valign="top" style="border-top: 1px solid gray; border-bottom: 1px solid gray;">
|
||||||
|
<strong>Extra:</strong>
|
||||||
|
</td>
|
||||||
|
<td style="border-top: 1px solid gray; border-bottom: 1px solid gray;">
|
||||||
|
<dl>
|
||||||
|
{% for k, v in extra.items() %}
|
||||||
|
<dt>{{ k }}</dt>
|
||||||
|
<dd>{{ v }}</dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
</td>
|
||||||
|
</tr
|
||||||
|
{% endif %}>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
|
@ -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 %}----------
|
|
@ -0,0 +1 @@
|
||||||
|
[Taiga] Feedback from {{ feedback_entry.full_name }} <{{ feedback_entry.email }}>
|
|
@ -128,4 +128,11 @@ router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links")
|
||||||
|
|
||||||
# Notify policies
|
# Notify policies
|
||||||
from taiga.projects.notifications.api import NotifyPolicyViewSet
|
from taiga.projects.notifications.api import NotifyPolicyViewSet
|
||||||
|
|
||||||
router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notifications")
|
router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notifications")
|
||||||
|
|
||||||
|
|
||||||
|
# feedback
|
||||||
|
# - see taiga.feedback.routers and taiga.feedback.apps
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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]
|
|
@ -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()
|
Loading…
Reference in New Issue