Merge pull request #867 from taigaio/us/2119/get-in-touch-with-the-project-admins

US 2219: Get in touch with the project admins
remotes/origin/issue/4795/notification_even_they_are_disabled
David Barragán Merino 2016-11-10 20:40:10 +01:00 committed by GitHub
commit bebee6c8f3
20 changed files with 479 additions and 11 deletions

View File

@ -1,5 +1,9 @@
# Changelog #
## 3.1.0 ¿¿?? (¿¿??)
### Features
- Contact with the project: if the projects have this module enabled Taiga users can contact them.
## 3.0.0 Stellaria Borealis (2016-10-02)

View File

@ -306,6 +306,7 @@ INSTALLED_APPS = [
"taiga.projects.tasks",
"taiga.projects.issues",
"taiga.projects.wiki",
"taiga.projects.contact",
"taiga.searches",
"taiga.timeline",
"taiga.mdrender",

View File

@ -157,7 +157,6 @@
padding-bottom:20px;
padding-left:20px;
padding-top:20px;
text-align:center;
}
/**
@ -188,6 +187,13 @@
background: #aad400;
}
hr {
width: 90%;
margin: 0 auto;
border-bottom: 1px solid #e1e1e1;
border-top: 0;
}
.bodyContent img{
display:inline;
height:auto;
@ -392,20 +398,29 @@
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateHeader">
<tr>
<td valign="top" class="headerContent">
<img src="{{ static("emails/top-bg-update.png") }}" />
<a href="{{ resolve_front_url("home") }}" title="Taiga">
<img src="{{ static("emails/logo-color.png") }}" id="headerImage" alt="Taiga logo" />
<img src="{{ static('emails/top-bg-update.png') }}" >
<a href="{{ resolve_front_url('home') }}" title="Taiga">
<img src="{{ static('emails/logo-color.png') }}" id="headerImage" alt="Taiga logo" />
</a>
{% block body %}
{% endblock %}
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center" valign="top">
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateBody">
<tr>
<td valign="top" class="bodyContent">
{% block body %}{% endblock %}
</td>
</tr>
{% block social %}
<tr>
<td valign="top" class="social-links">
<a href="{{ sr("social.twitter_url") }}" title="{{ _("Follow us on Twitter") }}" style="color: #9dce0a">{{ _("Twitter") }}</a>
<a href="{{ sr("social.github_url") }}" title="{{ _("Get the code on GitHub") }}" style="color: #9dce0a">{{ _("GitHub") }}</a>
<a href="{{ sr("taigaio_url") }}" title="{{ _("Visit our website") }}" style="color: #9dce0a">{{ _("Taiga.io") }}</a>
<a href="{{ sr('social.twitter_url') }}" title="{{ _('Follow us on Twitter') }}" style="color: #9dce0a">{{ _("Twitter") }}</a>
<a href="{{ sr('social.github_url') }}" title="{{ _('Get the code on GitHub') }}" style="color: #9dce0a">{{ _("GitHub") }}</a>
<a href="{{ sr('taigaio_url') }}" title="{{ _('Visit our website') }}" style="color: #9dce0a">{{ _("Taiga.io") }}</a>
</td>
</tr>
{% endblock %}

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# 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.contrib import admin
from . import models
class ContactEntryAdmin(admin.ModelAdmin):
list_display = ["created_date", "project", "user"]
list_display_links = list_display
list_filter = ["created_date", ]
date_hierarchy = "created_date"
ordering = ("-created_date", "id")
search_fields = ("project__name", "project__slug", "user__username", "user__email", "user__full_name")
admin.site.register(models.ContactEntry, ContactEntryAdmin)

View File

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# 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 status
from taiga.base.api.mixins import CreateModelMixin, BlockedByProjectMixin
from taiga.base.api.viewsets import GenericViewSet
from . import models
from . import permissions
from . import services
from . import validators
from django.conf import settings
class ContactViewSet(BlockedByProjectMixin, CreateModelMixin, GenericViewSet):
permission_classes = (permissions.ContactPermission,)
validator_class = validators.ContactEntryValidator
model = models.ContactEntry
def create(self, *args, **kwargs):
response = super().create(*args, **kwargs)
if response.status_code == status.HTTP_201_CREATED:
if settings.CELERY_ENABLED:
services.send_contact_email.delay(self.object.id)
else:
services.send_contact_email(self.object.id)
return response
def pre_save(self, obj):
obj.user = self.request.user
super().pre_save(obj)

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-11-10 15:18
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('projects', '0056_auto_20161110_1518'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ContactEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('comment', models.TextField(verbose_name='comment')),
('created_date', models.DateTimeField(auto_now_add=True, verbose_name='created date')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contact_entries', to='projects.Project', verbose_name='project')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contact_entries', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'contact entry',
'ordering': ['-created_date', 'id'],
'verbose_name_plural': 'contact entries',
},
),
]

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# 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 django.db import models
from django.utils.translation import ugettext_lazy as _
class ContactEntry(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="contact_entries",
verbose_name=_("user"))
project = models.ForeignKey("projects.Project", null=False, blank=False,
related_name="contact_entries", verbose_name=_("project"))
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 = "contact entry"
verbose_name_plural = "contact entries"
ordering = ["-created_date", "id"]

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# 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 PermissionComponent
from taiga.base.api.permissions import TaigaResourcePermission
class IsContactActivated(PermissionComponent):
def check_permissions(self, request, view, obj=None):
return request.user.is_authenticated() and obj.project.is_contact_activated
class ContactPermission(TaigaResourcePermission):
create_perms = IsContactActivated()

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# 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.mails import mail_builder
from taiga.celery import app
from taiga.front.templatetags.functions import resolve as resolve_front_url
from taiga.users.services import get_user_photo_url
from . import models
@app.task
def send_contact_email(contact_entry_id):
contact_entry = models.ContactEntry.objects.filter(id=contact_entry_id).first()
if contact_entry is None:
return
ctx = {
"comment": contact_entry.comment,
"full_name": contact_entry.user.get_full_name(),
"project_name": contact_entry.project.name,
"photo_url": get_user_photo_url(contact_entry.user),
"user_profile_url": resolve_front_url("user", contact_entry.user.username),
"project_settings_url": resolve_front_url("project-admin", contact_entry.project.slug),
}
users = contact_entry.project.get_users().exclude(id=contact_entry.user_id)
addresses = ", ".join([u.email for u in users])
email = mail_builder.contact_notification(addresses, ctx)
email.extra_headers["Reply-To"] = ", ".join([contact_entry.user.email])
email.send()

View File

@ -0,0 +1,24 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
<p>
{% if photo_url %}
<a href="{{ user_profile_url }}" title="{{ full_name }}" >
<img style="vertical-align:middle;" src="{{ photo_url }}" alt="{{ full_name }}" width="60"/>
</a>
{% endif %}
{% trans full_name=full_name, comment=comment, project_name=project_name %}
<a style="font-weight: bold;text-decoration:none;color:#222;" href="{{ user_profile_url }}" title="{{ full_name }}">{{ full_name }}</a> has written to {{ project_name }}
{% endtrans %}
</p>
<p>{{ comment }}</p>
<hr>
<p>
{% trans project_name=project_name %}
You are receiving this message because you are listed as administrator of the project titled {{ project_name }}. If you don't want members of the Taiga community contacting your project, please <a href="{{ project_settings_url }}">update your project settings</a> to prevent such contacts. Regular communications amongst members of the project will not be affected.
{% endtrans %}
</p>
{% endblock %}

View File

@ -0,0 +1,9 @@
{% trans full_name=full_name, comment=comment, project_name=project_name %}
{{ full_name }} has written to {{ project_name }}
{% endtrans %}
---------
{{ comment }}
---------
{% trans project_name=project_name %}
You are receiving this message because you are listed as administrator of the project titled {{ project_name }}. If you don't want members of the Taiga community contacting your project, please <a href="{{ project_settings_url }}">update your project settings</a< to prevent such contacts. Regular communications amongst members of the project will not be affected.
{% endtrans %}

View File

@ -0,0 +1,3 @@
{% trans full_name=full_name|safe, project_name=project_name|safe %}
[Taiga] {{ full_name }} has sent a message to the project {{ project_name }}
{% endtrans %}

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# 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 import validators
from . import models
class ContactEntryValidator(validators.ModelValidator):
class Meta:
model = models.ContactEntry
read_only_fields = ("user", "created_date", )

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-11-10 15:18
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('projects', '0055_json_to_jsonb'),
]
operations = [
migrations.AddField(
model_name='project',
name='is_contact_activated',
field=models.BooleanField(default=True, verbose_name='active contact'),
),
migrations.AddField(
model_name='projecttemplate',
name='is_contact_activated',
field=models.BooleanField(default=True, verbose_name='active contact'),
),
]

View File

@ -164,7 +164,8 @@ class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model):
total_milestones = models.IntegerField(null=True, blank=True,
verbose_name=_("total of milestones"))
total_story_points = models.FloatField(null=True, blank=True, verbose_name=_("total story points"))
is_contact_activated = models.BooleanField(default=True, null=False, blank=True,
verbose_name=_("active contact"))
is_epics_activated = models.BooleanField(default=False, null=False, blank=True,
verbose_name=_("active epics panel"))
is_backlog_activated = models.BooleanField(default=True, null=False, blank=True,
@ -734,7 +735,8 @@ class ProjectTemplate(models.Model):
default_owner_role = models.CharField(max_length=50, null=False,
blank=False,
verbose_name=_("default owner's role"))
is_contact_activated = models.BooleanField(default=True, null=False, blank=True,
verbose_name=_("active contact"))
is_epics_activated = models.BooleanField(default=False, null=False, blank=True,
verbose_name=_("active epics panel"))
is_backlog_activated = models.BooleanField(default=True, null=False, blank=True,
@ -780,6 +782,7 @@ class ProjectTemplate(models.Model):
super().save(*args, **kwargs)
def load_data_from_project(self, project):
self.is_contact_activated = project.is_contact_activated
self.is_epics_activated = project.is_epics_activated
self.is_backlog_activated = project.is_backlog_activated
self.is_kanban_activated = project.is_kanban_activated
@ -896,6 +899,7 @@ class ProjectTemplate(models.Model):
raise Exception("Project need an id (must be a saved project)")
project.creation_template = self
project.is_contact_activated = self.is_contact_activated
project.is_epics_activated = self.is_epics_activated
project.is_backlog_activated = self.is_backlog_activated
project.is_kanban_activated = self.is_kanban_activated

View File

@ -213,6 +213,7 @@ class ProjectSerializer(serializers.LightSerializer):
members = MethodField()
total_milestones = Field()
total_story_points = Field()
is_contact_activated = Field()
is_epics_activated = Field()
is_backlog_activated = Field()
is_kanban_activated = Field()
@ -474,6 +475,7 @@ class ProjectTemplateSerializer(serializers.LightSerializer):
created_date = Field()
modified_date = Field()
default_owner_role = Field()
is_contact_activated = Field()
is_epics_activated = Field()
is_backlog_activated = Field()
is_kanban_activated = Field()

View File

@ -223,6 +223,10 @@ router.register(r"history/task", TaskHistory, base_name="task-history")
router.register(r"history/issue", IssueHistory, base_name="issue-history")
router.register(r"history/wiki", WikiHistory, base_name="wiki-history")
# Contact
from taiga.projects.contact.api import ContactViewSet
router.register(r"contact", ContactViewSet, base_name="contact")
# Timelines
from taiga.timeline.api import ProfileTimeline

View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
# 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.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()
m.project = f.ProjectFactory.create()
f.MembershipFactory(user=m.project.owner, project=m.project, is_admin=True)
return m
def test_contact_create(client, data):
url = reverse("contact-list")
users = [None, data.user]
contact_data = json.dumps({
"project": data.project.id,
"comment": "Testing comment"
})
results = helper_test_http_method(client, 'post', url, contact_data, users)
assert results == [401, 201]

View File

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
# 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.core import mail
from django.core.urlresolvers import reverse
from tests import factories as f
from taiga.base.utils import json
import pytest
pytestmark = pytest.mark.django_db
def test_create_comment(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create()
f.MembershipFactory(user=project.owner, project=project, is_admin=True)
url = reverse("contact-list")
contact_data = json.dumps({
"project": project.id,
"comment": "Testing comment"
})
client.login(user)
assert len(mail.outbox) == 0
response = client.post(url, contact_data, content_type="application/json")
assert response.status_code == 201
assert len(mail.outbox) == 1
assert mail.outbox[0].to == [project.owner.email]
def test_create_comment_disabled(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create()
project.is_contact_activated = False
project.save()
f.MembershipFactory(user=project.owner, project=project, is_admin=True)
url = reverse("contact-list")
contact_data = json.dumps({
"project": project.id,
"comment": "Testing comment"
})
client.login(user)
response = client.post(url, contact_data, content_type="application/json")
assert response.status_code == 403