Browse Source

[IMP] pre-commit run

pull/50/head
Jairo Llopis 4 years ago
parent
commit
0f6487f338
No known key found for this signature in database GPG Key ID: 8B8A6900E4831A9B
  1. 6
      privacy_consent/__manifest__.py
  2. 30
      privacy_consent/controllers/main.py
  3. 9
      privacy_consent/data/ir_actions_server.xml
  4. 7
      privacy_consent/data/ir_cron.xml
  5. 116
      privacy_consent/data/mail.xml
  6. 33
      privacy_consent/models/mail_mail.py
  7. 24
      privacy_consent/models/mail_template.py
  8. 84
      privacy_consent/models/privacy_activity.py
  9. 61
      privacy_consent/models/privacy_consent.py
  10. 13
      privacy_consent/models/res_partner.py
  11. 10
      privacy_consent/templates/assets.xml
  12. 17
      privacy_consent/templates/form.xml
  13. 180
      privacy_consent/tests/test_consent.py
  14. 28
      privacy_consent/views/privacy_activity.xml
  15. 49
      privacy_consent/views/privacy_consent.xml
  16. 12
      privacy_consent/views/res_partner.xml
  17. 1
      setup/privacy_consent/odoo/addons/privacy_consent
  18. 6
      setup/privacy_consent/setup.py

6
privacy_consent/__manifest__.py

@ -3,7 +3,7 @@
{
"name": "Privacy - Consent",
"summary": "Allow people to explicitly accept or reject inclusion "
"in some activity, GDPR compliant",
"in some activity, GDPR compliant",
"version": "12.0.1.1.0",
"development_status": "Production/Stable",
"category": "Privacy",
@ -12,9 +12,7 @@
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": [
"privacy",
],
"depends": ["privacy"],
"data": [
"security/ir.model.access.csv",
"data/ir_actions_server.xml",

30
privacy_consent/controllers/main.py

@ -11,9 +11,12 @@ from odoo.addons.web.controllers.main import ensure_db
class ConsentController(Controller):
@route("/privacy/consent/<any(accept,reject):choice>/"
"<int:consent_id>/<token>",
type="http", auth="none", website=True)
@route(
"/privacy/consent/<any(accept,reject):choice>/" "<int:consent_id>/<token>",
type="http",
auth="none",
website=True,
)
def consent(self, choice, consent_id, token, *args, **kwargs):
"""Process user's consent acceptance or rejection."""
ensure_db()
@ -23,23 +26,26 @@ class ConsentController(Controller):
except AttributeError:
# If there's no website, the default is OK
pass
consent = request.env["privacy.consent"] \
.with_context(subject_answering=True) \
.sudo().browse(consent_id)
consent = (
request.env["privacy.consent"]
.with_context(subject_answering=True)
.sudo()
.browse(consent_id)
)
if not (consent.exists() and consent._token() == token):
raise NotFound
if consent.partner_id.lang:
consent = consent.with_context(lang=consent.partner_id.lang)
request.context = consent.env.context
consent.action_answer(choice == "accept", self._metadata())
return request.render("privacy_consent.form", {
"consent": consent,
})
return request.render("privacy_consent.form", {"consent": consent})
def _metadata(self):
return (u"User agent: {}\n"
u"Remote IP: {}\n"
u"Date and time: {:%Y-%m-%d %H:%M:%S}").format(
return (
u"User agent: {}\n"
u"Remote IP: {}\n"
u"Date and time: {:%Y-%m-%d %H:%M:%S}"
).format(
request.httprequest.environ.get("HTTP_USER_AGENT"),
request.httprequest.environ.get("REMOTE_ADDRESS"),
datetime.now(),

9
privacy_consent/data/ir_actions_server.xml

@ -1,14 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2019 initOS GmbH - Florian Kantelberg
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data>
<record id="sync_blacklist" model="ir.actions.server">
<field name="name">Sync partner's email blacklist status</field>
<field name="model_id" ref="model_privacy_consent"/>
<field name="crud_model_id" ref="base.model_res_partner"/>
<field name="model_id" ref="model_privacy_consent" />
<field name="crud_model_id" ref="base.model_res_partner" />
<field name="state">code</field>
<field name="code">
for consent in records:
@ -25,5 +23,4 @@
method(email)
</field>
</record>
</data>

7
privacy_consent/data/ir_cron.xml

@ -1,18 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2019 initOS GmbH - Florian Kantelberg
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data>
<record id="cron_auto_consent" model="ir.cron">
<field name="name">Request automatic data processing consents</field>
<field name="model_id" ref="model_privacy_activity"/>
<field name="model_id" ref="model_privacy_activity" />
<field name="state">code</field>
<field name="code">model._cron_new_consents()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
</record>
</data>

116
privacy_consent/data/mail.xml

@ -1,42 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2019 Tecnativa - Cristina Martin R.
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data>
<!-- Mail templates -->
<record id="template_consent" model="mail.template">
<field name="auto_delete" eval="False"/>
<field name="auto_delete" eval="False" />
<field name="name">Personal data processing consent request</field>
<field name="subject">Data processing consent request for ${object.activity_id.display_name|safe}</field>
<field name="model_id" ref="model_privacy_consent"/>
<field name="use_default_to" eval="True"/>
<field
name="subject"
>Data processing consent request for ${object.activity_id.display_name|safe}</field>
<field name="model_id" ref="model_privacy_consent" />
<field name="use_default_to" eval="True" />
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html" type="xml">
<div style="background:#F3F5F6;color:#515166;padding:25px 0px;font-family:Arial,Helvetica,sans-serif;font-size:14px;">
<div
style="background:#F3F5F6;color:#515166;padding:25px 0px;font-family:Arial,Helvetica,sans-serif;font-size:14px;"
>
<table style="width:600px;margin:5px auto;">
<tbody>
<tr>
<td>
<a href="/">
<img src="/logo" alt="${object.activity_id.controller_id.display_name|safe}" style="vertical-align:baseline;max-width:100px;"/>
<img
src="/logo"
alt="${object.activity_id.controller_id.display_name|safe}"
style="vertical-align:baseline;max-width:100px;"
/>
</a>
</td>
</tr>
</tbody>
</table>
<table style="width:600px;margin:0px auto;background:white;border:1px solid #e1e1e1;">
<table
style="width:600px;margin:0px auto;background:white;border:1px solid #e1e1e1;"
>
<tbody>
<tr>
<td colspan="2" style="padding:15px 20px 0px 20px; font-size:16px;">
<td
colspan="2"
style="padding:15px 20px 0px 20px; font-size:16px;"
>
<p>
Hello, ${object.partner_id.name|safe}
</p>
<p>
We contacted you to ask you to give us your explicit consent to include your data in a data processing activity called
<b>${object.activity_id.display_name|safe}</b>, property of
<i>${object.activity_id.controller_id.display_name|safe}</i>
<b
>${object.activity_id.display_name|safe}</b>, property of
<i
>${object.activity_id.controller_id.display_name|safe}</i>
</p>
${object.description or ""}
<p>
@ -59,19 +72,32 @@
</td>
</tr>
<tr>
<td style="padding:15px 20px 0px 20px; font-size:16px; text-align:right;">
<a href="/privacy/consent/accept/" style="background-color: #449d44; padding: 12px; font-weight: 12px; text-decoration: none; color: #fff; border-radius: 5px; font-size:16px;">
<td
style="padding:15px 20px 0px 20px; font-size:16px; text-align:right;"
>
<a
href="/privacy/consent/accept/"
style="background-color: #449d44; padding: 12px; font-weight: 12px; text-decoration: none; color: #fff; border-radius: 5px; font-size:16px;"
>
Accept
</a>
</td>
<td style="padding:15px 20px 0px 20px; font-size:16px; text-align:left;">
<a href="/privacy/consent/reject/" style="background-color: #d9534f; padding: 12px; font-weight: 12px; text-decoration: none; color: #fff; border-radius: 5px; font-size:16px;">
<td
style="padding:15px 20px 0px 20px; font-size:16px; text-align:left;"
>
<a
href="/privacy/consent/reject/"
style="background-color: #d9534f; padding: 12px; font-weight: 12px; text-decoration: none; color: #fff; border-radius: 5px; font-size:16px;"
>
Reject
</a>
</td>
</tr>
<tr>
<td colspan="2" style="padding:15px 20px 15px 20px; font-size:16px;">
<td
colspan="2"
style="padding:15px 20px 15px 20px; font-size:16px;"
>
<p>
If you need further information, please respond to this email and we will attend your request as soon as possible.
</p>
@ -88,7 +114,10 @@
<td style="padding-top:10px;font-size: 12px;">
<p>
Sent by
<a href="/" style="color:#717188;">${object.activity_id.controller_id.display_name|safe}</a>.
<a
href="/"
style="color:#717188;"
>${object.activity_id.controller_id.display_name|safe}</a>.
</p>
</td>
</tr>
@ -97,62 +126,61 @@
</div>
</field>
</record>
<!-- Mail subtypes -->
<record id="mt_consent_consent_new" model="mail.message.subtype">
<field name="name">New Consent</field>
<field name="description">Privacy consent request created</field>
<field name="res_model">privacy.consent</field>
<field name="default" eval="False"/>
<field name="hidden" eval="False"/>
<field name="internal" eval="True"/>
<field name="default" eval="False" />
<field name="hidden" eval="False" />
<field name="internal" eval="True" />
</record>
<record id="mt_consent_acceptance_changed" model="mail.message.subtype">
<field name="name">Acceptance Changed by Subject</field>
<field name="description">Acceptance status updated by subject</field>
<field name="res_model">privacy.consent</field>
<field name="default" eval="False"/>
<field name="hidden" eval="False"/>
<field name="internal" eval="True"/>
<field name="default" eval="False" />
<field name="hidden" eval="False" />
<field name="internal" eval="True" />
</record>
<record id="mt_consent_state_changed" model="mail.message.subtype">
<field name="name">State Changed</field>
<field name="description">Privacy consent request state changed</field>
<field name="res_model">privacy.consent</field>
<field name="default" eval="False"/>
<field name="hidden" eval="False"/>
<field name="internal" eval="True"/>
<field name="default" eval="False" />
<field name="hidden" eval="False" />
<field name="internal" eval="True" />
</record>
<record id="mt_activity_consent_new" model="mail.message.subtype">
<field name="name">New Consent</field>
<field name="description">Privacy consent request created</field>
<field name="res_model">privacy.activity</field>
<field name="default" eval="True"/>
<field name="hidden" eval="False"/>
<field name="internal" eval="True"/>
<field name="parent_id" ref="mt_consent_consent_new"/>
<field name="default" eval="True" />
<field name="hidden" eval="False" />
<field name="internal" eval="True" />
<field name="parent_id" ref="mt_consent_consent_new" />
<field name="relation_field">activity_id</field>
</record>
<record id="mt_activity_acceptance_changed" model="mail.message.subtype">
<field name="name">Acceptance Changed</field>
<field name="description">Privacy consent request acceptance status changed</field>
<field
name="description"
>Privacy consent request acceptance status changed</field>
<field name="res_model">privacy.activity</field>
<field name="default" eval="True"/>
<field name="hidden" eval="False"/>
<field name="internal" eval="True"/>
<field name="parent_id" ref="mt_consent_acceptance_changed"/>
<field name="default" eval="True" />
<field name="hidden" eval="False" />
<field name="internal" eval="True" />
<field name="parent_id" ref="mt_consent_acceptance_changed" />
<field name="relation_field">activity_id</field>
</record>
<record id="mt_activity_state_changed" model="mail.message.subtype">
<field name="name">State Changed</field>
<field name="description">Privacy consent request state changed</field>
<field name="res_model">privacy.activity</field>
<field name="default" eval="False"/>
<field name="hidden" eval="False"/>
<field name="internal" eval="True"/>
<field name="parent_id" ref="mt_consent_state_changed"/>
<field name="default" eval="False" />
<field name="hidden" eval="False" />
<field name="internal" eval="True" />
<field name="parent_id" ref="mt_consent_state_changed" />
<field name="relation_field">activity_id</field>
</record>
</data>

33
privacy_consent/models/mail_mail.py

@ -7,8 +7,9 @@ from odoo import models
class MailMail(models.Model):
_inherit = "mail.mail"
def _postprocess_sent_message(self, success_pids, failure_reason=False,
failure_type=None):
def _postprocess_sent_message(
self, success_pids, failure_reason=False, failure_type=None
):
"""Write consent status after sending message."""
# Know if mail was successfully sent to a privacy consent
if (
@ -20,17 +21,13 @@ class MailMail(models.Model):
):
# Get related consent
consent = self.env["privacy.consent"].browse(
self.mail_message_id.res_id,
self._prefetch,
self.mail_message_id.res_id, self._prefetch,
)
# Set as sent if needed
if (
consent.state == "draft"
and consent.partner_id.id in {par.id for par in success_pids}
):
consent.write({
"state": "sent",
})
if consent.state == "draft" and consent.partner_id.id in {
par.id for par in success_pids
}:
consent.write({"state": "sent"})
return super()._postprocess_sent_message(
success_pids=success_pids,
failure_reason=failure_reason,
@ -51,15 +48,11 @@ class MailMail(models.Model):
if self.model != "privacy.consent":
return result
# Tokenize consent links
consent = self.env["privacy.consent"] \
.browse(self.mail_message_id.res_id) \
consent = (
self.env["privacy.consent"]
.browse(self.mail_message_id.res_id)
.with_prefetch(self._prefetch)
result = result.replace(
"/privacy/consent/accept/",
consent._url(True),
)
result = result.replace(
"/privacy/consent/reject/",
consent._url(False),
)
result = result.replace("/privacy/consent/accept/", consent._url(True),)
result = result.replace("/privacy/consent/reject/", consent._url(False),)
return result

24
privacy_consent/models/mail_template.py

@ -13,20 +13,22 @@ class MailTemplate(models.Model):
@api.constrains("body_html", "model")
def _check_consent_links_in_body_html(self):
"""Body for ``privacy.consent`` templates needs placeholder links."""
links = ["//a[@href='/privacy/consent/{}/']".format(action)
for action in ("accept", "reject")]
links = [
"//a[@href='/privacy/consent/{}/']".format(action)
for action in ("accept", "reject")
]
for one in self:
if one.model != "privacy.consent":
continue
doc = html.document_fromstring(one.body_html)
for link in links:
if not doc.xpath(link):
raise ValidationError(_(
"Missing privacy consent link placeholders. "
"You need at least these two links:\n"
'<a href="%s">Accept</a>\n'
'<a href="%s">Reject</a>'
) % (
"/privacy/consent/accept/",
"/privacy/consent/reject/",
))
raise ValidationError(
_(
"Missing privacy consent link placeholders. "
"You need at least these two links:\n"
'<a href="%s">Accept</a>\n'
'<a href="%s">Reject</a>'
)
% ("/privacy/consent/accept/", "/privacy/consent/reject/",)
)

84
privacy_consent/models/privacy_activity.py

@ -7,48 +7,36 @@ from odoo.tools.safe_eval import safe_eval
class PrivacyActivity(models.Model):
_inherit = 'privacy.activity'
_inherit = "privacy.activity"
server_action_id = fields.Many2one(
"ir.actions.server",
"Server action",
domain=[
("model_id.model", "=", "privacy.consent"),
],
domain=[("model_id.model", "=", "privacy.consent")],
help="Run this action when a new consent request is created or its "
"acceptance status is updated.",
)
consent_ids = fields.One2many(
"privacy.consent",
"activity_id",
"Consents",
)
consent_count = fields.Integer(
"Consents count",
compute="_compute_consent_count",
"acceptance status is updated.",
)
consent_ids = fields.One2many("privacy.consent", "activity_id", "Consents",)
consent_count = fields.Integer("Consents count", compute="_compute_consent_count",)
consent_required = fields.Selection(
[("auto", "Automatically"), ("manual", "Manually")],
"Ask subjects for consent",
help="Enable if you need to track any kind of consent "
"from the affected subjects",
"from the affected subjects",
)
consent_template_id = fields.Many2one(
"mail.template",
"Email template",
default=lambda self: self._default_consent_template_id(),
domain=[
("model", "=", "privacy.consent"),
],
domain=[("model", "=", "privacy.consent")],
help="Email to be sent to subjects to ask for consent. "
"A good template should include details about the current "
"consent request status, how to change it, and where to "
"get more information.",
"A good template should include details about the current "
"consent request status, how to change it, and where to "
"get more information.",
)
default_consent = fields.Boolean(
"Accepted by default",
help="Should we assume the subject has accepted if we receive no "
"response?",
help="Should we assume the subject has accepted if we receive no " "response?",
)
# Hidden helpers help user design new templates
@ -66,40 +54,43 @@ class PrivacyActivity(models.Model):
@api.depends("consent_ids")
def _compute_consent_count(self):
groups = self.env["privacy.consent"].read_group(
[("activity_id", "in", self.ids)],
["activity_id"],
["activity_id"],
[("activity_id", "in", self.ids)], ["activity_id"], ["activity_id"],
)
for group in groups:
self.browse(group["activity_id"][0], self._prefetch) \
.consent_count = group["activity_id_count"]
self.browse(group["activity_id"][0], self._prefetch).consent_count = group[
"activity_id_count"
]
def _compute_consent_template_defaults(self):
"""Used in context values, to help users design new templates."""
template = self._default_consent_template_id()
if template:
self.update({
"consent_template_default_body_html": template.body_html,
"consent_template_default_subject": template.subject,
})
self.update(
{
"consent_template_default_body_html": template.body_html,
"consent_template_default_subject": template.subject,
}
)
@api.constrains("consent_required", "consent_template_id")
def _check_auto_consent_has_template(self):
"""Require a mail template to automate consent requests."""
for one in self:
if one.consent_required == "auto" and not one.consent_template_id:
raise ValidationError(_(
"Specify a mail template to ask automated consent."
))
raise ValidationError(
_("Specify a mail template to ask automated consent.")
)
@api.constrains("consent_required", "subject_find")
def _check_consent_required_subject_find(self):
for one in self:
if one.consent_required and not one.subject_find:
raise ValidationError(_(
"Require consent is available only for subjects "
"in current database."
))
raise ValidationError(
_(
"Require consent is available only for subjects "
"in current database."
)
)
@api.model
def _cron_new_consents(self):
@ -117,19 +108,20 @@ class PrivacyActivity(models.Model):
"""Generate new consent requests."""
consents_vals = []
# Skip activitys where consent is not required
for one in self.with_context(active_test=False) \
.filtered("consent_required"):
for one in self.with_context(active_test=False).filtered("consent_required"):
domain = [
("id", "not in", one.mapped("consent_ids.partner_id").ids),
("email", "!=", False),
] + safe_eval(one.subject_domain)
# Store values for creating missing consent requests
for missing in self.env["res.partner"].search(domain):
consents_vals.append({
"partner_id": missing.id,
"accepted": one.default_consent,
"activity_id": one.id,
})
consents_vals.append(
{
"partner_id": missing.id,
"accepted": one.default_consent,
"activity_id": one.id,
}
)
# Create and send consent request emails for automatic activitys
consents = self.env["privacy.consent"].create(consents_vals)
consents.action_auto_ask()

61
privacy_consent/models/privacy_consent.py

@ -8,24 +8,24 @@ from odoo import api, fields, models
class PrivacyConsent(models.Model):
_name = 'privacy.consent'
_name = "privacy.consent"
_description = "Consent of data processing"
_inherit = "mail.thread"
_rec_name = "partner_id"
_sql_constraints = [
("unique_partner_activity", "UNIQUE(partner_id, activity_id)",
"Duplicated partner in this data processing activity"),
(
"unique_partner_activity",
"UNIQUE(partner_id, activity_id)",
"Duplicated partner in this data processing activity",
),
]
active = fields.Boolean(
default=True,
index=True,
)
active = fields.Boolean(default=True, index=True,)
accepted = fields.Boolean(
track_visibility="onchange",
help="Indicates current acceptance status, which can come from "
"subject's last answer, or from the default specified in the "
"related data processing activity.",
"subject's last answer, or from the default specified in the "
"related data processing activity.",
)
last_metadata = fields.Text(
readonly=True,
@ -71,18 +71,12 @@ class PrivacyConsent(models.Model):
def _token(self):
"""Secret token to publicly authenticate this record."""
secret = self.env["ir.config_parameter"].sudo().get_param(
"database.secret")
secret = self.env["ir.config_parameter"].sudo().get_param("database.secret")
params = "{}-{}-{}-{}".format(
self.env.cr.dbname,
self.id,
self.partner_id.id,
self.activity_id.id,
self.env.cr.dbname, self.id, self.partner_id.id, self.activity_id.id,
)
return hmac.new(
secret.encode('utf-8'),
params.encode('utf-8'),
hashlib.sha512,
secret.encode("utf-8"), params.encode("utf-8"), hashlib.sha512,
).hexdigest()
def _url(self, accept):
@ -100,9 +94,11 @@ class PrivacyConsent(models.Model):
def _send_consent_notification(self):
"""Send email notification to subject."""
for one in self.with_context(tpl_force_default_to=True,
mail_notify_user_signature=False,
mail_auto_subscribe_no_notify=True):
for one in self.with_context(
tpl_force_default_to=True,
mail_notify_user_signature=False,
mail_auto_subscribe_no_notify=True,
):
one.activity_id.consent_template_id.send_mail(one.id)
def _run_action(self):
@ -112,17 +108,14 @@ class PrivacyConsent(models.Model):
if one.state == "draft":
continue
action = one.activity_id.server_action_id.with_context(
active_id=one.id,
active_ids=one.ids,
active_model=one._name,
active_id=one.id, active_ids=one.ids, active_model=one._name,
)
action.run()
@api.model_create_multi
def create(self, vals_list):
"""Run server action on create."""
super_ = super(PrivacyConsent,
self.with_context(mail_create_nolog=True))
super_ = super(PrivacyConsent, self.with_context(mail_create_nolog=True))
results = super_.create(vals_list)
# Sync the default acceptance status
results.sudo()._run_action()
@ -135,14 +128,11 @@ class PrivacyConsent(models.Model):
return result
def message_get_suggested_recipients(self):
result = super() \
.message_get_suggested_recipients()
result = super().message_get_suggested_recipients()
reason = self._fields["partner_id"].string
for one in self:
one._message_add_suggested_recipient(
result,
partner=one.partner_id,
reason=reason,
result, partner=one.partner_id, reason=reason,
)
return result
@ -168,7 +158,8 @@ class PrivacyConsent(models.Model):
"""Automatically ask for consent."""
templated = self.filtered("activity_id.consent_template_id")
automated = templated.filtered(
lambda one: one.activity_id.consent_required == "auto")
lambda one: one.activity_id.consent_required == "auto"
)
automated._send_consent_notification()
def action_answer(self, answer, metadata=False):
@ -180,8 +171,4 @@ class PrivacyConsent(models.Model):
:param str metadata:
Metadata from last user acceptance or rejection request.
"""
self.write({
"state": "answered",
"accepted": answer,
"last_metadata": metadata,
})
self.write({"state": "answered", "accepted": answer, "last_metadata": metadata})

13
privacy_consent/models/res_partner.py

@ -8,9 +8,7 @@ class ResPartner(models.Model):
_inherit = "res.partner"
privacy_consent_ids = fields.One2many(
"privacy.consent",
"partner_id",
"Privacy consents",
"privacy.consent", "partner_id", "Privacy consents",
)
privacy_consent_count = fields.Integer(
"Consents",
@ -22,10 +20,9 @@ class ResPartner(models.Model):
def _compute_privacy_consent_count(self):
"""Count consent requests."""
groups = self.env["privacy.consent"].read_group(
[("partner_id", "in", self.ids)],
["partner_id"],
["partner_id"],
[("partner_id", "in", self.ids)], ["partner_id"], ["partner_id"],
)
for group in groups:
self.browse(group["partner_id"][0], self._prefetch) \
.privacy_consent_count = group["partner_id_count"]
self.browse(
group["partner_id"][0], self._prefetch
).privacy_consent_count = group["partner_id_count"]

10
privacy_consent/templates/assets.xml

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Jairo Llopis
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data>
<template id="assets_frontend" inherit_id="web.assets_frontend">
<xpath expr=".">
<link rel="stylesheet" href="/privacy_consent/static/src/css/privacy_consent.scss" />
<link
rel="stylesheet"
href="/privacy_consent/static/src/css/privacy_consent.scss"
/>
</xpath>
</template>
</data>

17
privacy_consent/templates/form.xml

@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data>
<template id="form" name="Consent response processed">
<!-- Use web.login_layout because it gets automatically wrapped
by website layout if website is installed, and otherwise includes
@ -14,13 +12,13 @@
<div class="jumbotron">
<h1>Thank you!</h1>
<p>
Hello, <b t-esc="consent.partner_id.display_name"/>
Hello, <b t-esc="consent.partner_id.display_name" />
</p>
<p>
We asked you to authorize us to process your data in this data processing activity:
<b t-esc="consent.activity_id.display_name"/>
<b t-esc="consent.activity_id.display_name" />
</p>
<t t-raw="consent.activity_id.description or ''"/>
<t t-raw="consent.activity_id.description or ''" />
<p t-if="consent.accepted">
You have <b class="text-success">accepted</b> such processing.
</p>
@ -53,12 +51,13 @@
Thanks for your response.
</p>
<p class="text-muted">
Sincerely,<br/>
<i t-raw="consent.activity_id.controller_id.with_context(show_address=True, html_format=True).name_get()[0][1]"/>
Sincerely,<br />
<i
t-raw="consent.activity_id.controller_id.with_context(show_address=True, html_format=True).name_get()[0][1]"
/>
</p>
</div>
</div>
</t>
</template>
</data>

180
privacy_consent/tests/test_consent.py

@ -4,110 +4,95 @@
from contextlib import contextmanager
from odoo.exceptions import ValidationError
from odoo.tests.common import HttpCase, Form
from odoo.tests.common import Form, HttpCase
class ActivityCase(HttpCase):
def setUp(self):
super(ActivityCase, self).setUp()
self.cron = self.env.ref("privacy_consent.cron_auto_consent")
self.cron_mail_queue = self.env.ref(
"mail.ir_cron_mail_scheduler_action")
self.cron_mail_queue = self.env.ref("mail.ir_cron_mail_scheduler_action")
self.sync_blacklist = self.env.ref("privacy_consent.sync_blacklist")
self.mt_consent_consent_new = self.env.ref(
"privacy_consent.mt_consent_consent_new")
"privacy_consent.mt_consent_consent_new"
)
self.mt_consent_acceptance_changed = self.env.ref(
"privacy_consent.mt_consent_acceptance_changed")
"privacy_consent.mt_consent_acceptance_changed"
)
self.mt_consent_state_changed = self.env.ref(
"privacy_consent.mt_consent_state_changed")
"privacy_consent.mt_consent_state_changed"
)
# Some partners to ask for consent
self.partners = self.env["res.partner"]
self.partners += self.partners.create({
"name": "consent-partner-0",
"email": "partner0@example.com",
})
self.partners += self.partners.create({
"name": "consent-partner-1",
"email": "partner1@example.com",
})
self.partners += self.partners.create({
"name": "consent-partner-2",
"email": "partner2@example.com",
})
self.partners += self.partners.create(
{"name": "consent-partner-0", "email": "partner0@example.com"}
)
self.partners += self.partners.create(
{"name": "consent-partner-1", "email": "partner1@example.com"}
)
self.partners += self.partners.create(
{"name": "consent-partner-2", "email": "partner2@example.com"}
)
# Partner without email, on purpose
self.partners += self.partners.create({
"name": "consent-partner-3",
})
self.partners += self.partners.create({"name": "consent-partner-3"})
# Partner with wrong email, on purpose
self.partners += self.partners.create({
"name": "consent-partner-4",
"email": "wrong-mail",
})
self.partners += self.partners.create(
{"name": "consent-partner-4", "email": "wrong-mail"}
)
# Blacklist some partners
self.blacklists = self.env["mail.blacklist"]
self.blacklists += self.blacklists._add("partner1@example.com")
# Activity without consent
self.activity_noconsent = self.env["privacy.activity"].create({
"name": "activity_noconsent",
"description": "I'm activity 1",
})
self.activity_noconsent = self.env["privacy.activity"].create(
{"name": "activity_noconsent", "description": "I'm activity 1"}
)
# Activity with auto consent, for all partners
self.activity_auto = self.env["privacy.activity"].create({
"name": "activity_auto",
"description": "I'm activity auto",
"subject_find": True,
"subject_domain": repr([("id", "in", self.partners.ids)]),
"consent_required": "auto",
"default_consent": True,
"server_action_id": self.sync_blacklist.id,
})
self.activity_auto = self.env["privacy.activity"].create(
{
"name": "activity_auto",
"description": "I'm activity auto",
"subject_find": True,
"subject_domain": repr([("id", "in", self.partners.ids)]),
"consent_required": "auto",
"default_consent": True,
"server_action_id": self.sync_blacklist.id,
}
)
# Activity with manual consent, skipping partner 0
self.activity_manual = self.env["privacy.activity"].create({
"name": "activity_manual",
"description": "I'm activity 3",
"subject_find": True,
"subject_domain": repr([("id", "in", self.partners[1:].ids)]),
"consent_required": "manual",
"default_consent": False,
"server_action_id": self.sync_blacklist.id,
})
self.activity_manual = self.env["privacy.activity"].create(
{
"name": "activity_manual",
"description": "I'm activity 3",
"subject_find": True,
"subject_domain": repr([("id", "in", self.partners[1:].ids)]),
"consent_required": "manual",
"default_consent": False,
"server_action_id": self.sync_blacklist.id,
}
)
@contextmanager
def _patch_build(self):
self._built_messages = []
IMS = self.env['ir.mail_server']
IMS = self.env["ir.mail_server"]
def _build_email(
_self,
email_from,
email_to,
subject,
body,
*args,
**kwargs
):
def _build_email(_self, email_from, email_to, subject, body, *args, **kwargs):
self._built_messages.append(body)
return _build_email.origin(
_self,
email_from,
email_to,
subject,
body,
*args,
**kwargs,
_self, email_from, email_to, subject, body, *args, **kwargs,
)
try:
IMS._patch_method('build_email', _build_email)
IMS._patch_method("build_email", _build_email)
yield
finally:
IMS._revert_method('build_email')
IMS._revert_method("build_email")
def check_activity_auto_properly_sent(self):
"""Check emails sent by ``self.activity_auto``."""
consents = self.env["privacy.consent"].search([
("activity_id", "=", self.activity_auto.id),
])
consents = self.env["privacy.consent"].search(
[("activity_id", "=", self.activity_auto.id)]
)
# Check pending mails
for consent in consents:
self.assertEqual(consent.state, "draft")
@ -116,40 +101,33 @@ class ActivityCase(HttpCase):
# Check sent mails
with self._patch_build():
self.cron_mail_queue.method_direct_trigger()
for index, consent in enumerate(consents):
for consent in consents:
good_email = "@" in (consent.partner_id.email or "")
expected_messages = 3 if good_email else 2
self.assertEqual(
consent.state,
"sent" if good_email else "draft",
consent.state, "sent" if good_email else "draft",
)
messages = consent.message_ids
self.assertEqual(len(messages), expected_messages)
# 2nd message notifies creation
self.assertEqual(
messages[expected_messages - 1].subtype_id,
self.mt_consent_consent_new,
messages[expected_messages - 1].subtype_id, self.mt_consent_consent_new,
)
# 3rd message notifies subject
# Placeholder links should be logged
self.assertIn(
"/privacy/consent/accept/",
messages[expected_messages - 2].body)
"/privacy/consent/accept/", messages[expected_messages - 2].body
)
self.assertIn(
"/privacy/consent/reject/",
messages[expected_messages - 2].body)
"/privacy/consent/reject/", messages[expected_messages - 2].body
)
# Tokenized links shouldn't be logged
self.assertNotIn(
consent._url(True),
messages[expected_messages - 2].body)
self.assertNotIn(
consent._url(False),
messages[expected_messages - 2].body)
self.assertNotIn(consent._url(True), messages[expected_messages - 2].body)
self.assertNotIn(consent._url(False), messages[expected_messages - 2].body)
# 4th message contains the state change
if good_email:
self.assertEqual(
messages[0].subtype_id,
self.mt_consent_state_changed,
messages[0].subtype_id, self.mt_consent_state_changed,
)
# Partner's is_blacklisted should be synced with default consent
self.assertFalse(consent.partner_id.is_blacklisted)
@ -166,23 +144,21 @@ class ActivityCase(HttpCase):
"""We have a good mail template by default."""
good = self.env.ref("privacy_consent.template_consent")
self.assertEqual(
self.activity_noconsent.consent_template_id,
good,
self.activity_noconsent.consent_template_id, good,
)
self.assertEqual(
self.activity_noconsent.consent_template_default_body_html,
good.body_html,
self.activity_noconsent.consent_template_default_body_html, good.body_html,
)
self.assertEqual(
self.activity_noconsent.consent_template_default_subject,
good.subject,
self.activity_noconsent.consent_template_default_subject, good.subject,
)
def test_find_subject_if_consent_required(self):
"""If user wants to require consent, it needs subjects."""
# Test the onchange helper
onchange_activity1 = self.env["privacy.activity"].new(
self.activity_noconsent.copy_data()[0])
self.activity_noconsent.copy_data()[0]
)
self.assertFalse(onchange_activity1.subject_find)
onchange_activity1.consent_required = "auto"
onchange_activity1._onchange_consent_required_subject_find()
@ -209,8 +185,7 @@ class ActivityCase(HttpCase):
consents = self.env[result["res_model"]].search(result["domain"])
self.assertEqual(consents.mapped("state"), ["draft"] * 3)
self.assertEqual(
consents.mapped("partner_id.is_blacklisted"),
[False] * 3,
consents.mapped("partner_id.is_blacklisted"), [False] * 3,
)
self.assertEqual(consents.mapped("accepted"), [False] * 3)
self.assertEqual(consents.mapped("last_metadata"), [False] * 3)
@ -243,8 +218,7 @@ class ActivityCase(HttpCase):
self.assertEqual(messages[0].subtype_id, self.mt_consent_state_changed)
self.assertEqual(consents.mapped("state"), ["sent", "draft", "draft"])
self.assertEqual(
consents.mapped("partner_id.is_blacklisted"),
[True, False, False],
consents.mapped("partner_id.is_blacklisted"), [True, False, False],
)
# Placeholder links should be logged
self.assertTrue("/privacy/consent/accept/" in messages[1].body)
@ -264,11 +238,9 @@ class ActivityCase(HttpCase):
self.assertEqual(consents.mapped("accepted"), [True, False, False])
self.assertTrue(consents[0].last_metadata)
self.assertFalse(consents[0].partner_id.is_blacklisted)
self.assertEqual(consents.mapped("state"), ["answered", "draft", "draft"])
self.assertEqual(
consents.mapped("state"), ["answered", "draft", "draft"])
self.assertEqual(
consents[0].message_ids[0].subtype_id,
self.mt_consent_acceptance_changed,
consents[0].message_ids[0].subtype_id, self.mt_consent_acceptance_changed,
)
# Visit tokenized reject URL
result = self.url_open(reject_url).text
@ -280,11 +252,9 @@ class ActivityCase(HttpCase):
self.assertEqual(consents.mapped("accepted"), [False, False, False])
self.assertTrue(consents[0].last_metadata)
self.assertTrue(consents[0].partner_id.is_blacklisted)
self.assertEqual(consents.mapped("state"), ["answered", "draft", "draft"])
self.assertEqual(
consents.mapped("state"), ["answered", "draft", "draft"])
self.assertEqual(
consents[0].message_ids[0].subtype_id,
self.mt_consent_acceptance_changed,
consents[0].message_ids[0].subtype_id, self.mt_consent_acceptance_changed,
)
self.assertFalse(consents[1].last_metadata)

28
privacy_consent/views/privacy_activity.xml

@ -1,13 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data>
<record id="activity_form" model="ir.ui.view">
<field name="name">Add consent fields</field>
<field name="model">privacy.activity</field>
<field name="inherit_id" ref="privacy.activity_form"/>
<field name="inherit_id" ref="privacy.activity_form" />
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button
@ -18,19 +16,15 @@
name="%(consent_action)d"
type="action"
>
<field
name="consent_count"
widget="statinfo"
/>
<field name="consent_count" widget="statinfo" />
</button>
</div>
<notebook name="advanced" position="inside">
<page string="Consent" name="consent">
<group>
<label for="consent_required"/>
<label for="consent_required" />
<div>
<field name="consent_required" class="oe_inline"/>
<field name="consent_required" class="oe_inline" />
<button
attrs='{"invisible": [("consent_required", "!=", "manual")]}'
class="btn-link"
@ -50,15 +44,10 @@
/>
</div>
</group>
<group
attrs='{"invisible": [("consent_required", "=", False)]}'
>
<group attrs='{"invisible": [("consent_required", "=", False)]}'>
<group>
<field name="default_consent"/>
<field
name="server_action_id"
groups="base.group_no_one"
/>
<field name="default_consent" />
<field name="server_action_id" groups="base.group_no_one" />
</group>
<group>
<field
@ -84,5 +73,4 @@
</notebook>
</field>
</record>
</data>

49
privacy_consent/views/privacy_consent.xml

@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data>
<record model="ir.ui.view" id="consent_form">
<field name="name">Privacy Consent Form</field>
<field name="model">privacy.consent</field>
@ -16,7 +14,7 @@
class="oe_highlight"
string="Ask for consent"
/>
<field name="state" widget="statusbar"/>
<field name="state" widget="statusbar" />
</header>
<sheet>
<div class="oe_button_box" name="button_box">
@ -34,49 +32,47 @@
</button>
</div>
<group>
<field name="partner_id"/>
<field name="activity_id"/>
<field name="accepted"/>
<field name="last_metadata"/>
<field name="partner_id" />
<field name="activity_id" />
<field name="accepted" />
<field name="last_metadata" />
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers" />
<field name="message_ids" widget="mail_thread" />
</div>
</form>
</field>
</record>
<record model="ir.ui.view" id="consent_tree">
<field name="name">Privacy Consent Tree</field>
<field name="model">privacy.consent</field>
<field name="arch" type="xml">
<tree>
<field name="activity_id"/>
<field name="partner_id"/>
<field name="state"/>
<field name="accepted"/>
<field name="activity_id" />
<field name="partner_id" />
<field name="state" />
<field name="accepted" />
</tree>
</field>
</record>
<record model="ir.ui.view" id="consent_search">
<field name="name">Privacy Consent Search</field>
<field name="model">privacy.consent</field>
<field name="arch" type="xml">
<search>
<field name="activity_id"/>
<field name="partner_id"/>
<field name="state"/>
<field name="accepted"/>
<separator/>
<field name="activity_id" />
<field name="partner_id" />
<field name="state" />
<field name="accepted" />
<separator />
<filter
string="Archived"
name="inactive"
domain="[('active', '=', False)]"
/>
<separator/>
<separator />
<group string="Group By" name="groupby">
<filter
name="activity_id_groupby"
@ -97,17 +93,10 @@
</search>
</field>
</record>
<act_window
id="consent_action"
name="Consents"
res_model="privacy.consent"
/>
<act_window id="consent_action" name="Consents" res_model="privacy.consent" />
<menuitem
action="consent_action"
id="menu_privacy_consent"
parent="privacy.menu_data_protection_master_data"
/>
</data>

12
privacy_consent/views/res_partner.xml

@ -1,13 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data>
<record id="view_partner_form" model="ir.ui.view">
<field name="name">Add consent smart button</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="inherit_id" ref="base.view_partner_form" />
<field
name="groups_id"
eval="[(4, ref('privacy.group_data_protection_user'))]"
@ -22,13 +20,9 @@
name="%(consent_action)d"
type="action"
>
<field
name="privacy_consent_count"
widget="statinfo"
/>
<field name="privacy_consent_count" widget="statinfo" />
</button>
</div>
</field>
</record>
</data>

1
setup/privacy_consent/odoo/addons/privacy_consent

@ -0,0 +1 @@
../../../../privacy_consent

6
setup/privacy_consent/setup.py

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
Loading…
Cancel
Save