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. 4
      privacy_consent/__manifest__.py
  2. 28
      privacy_consent/controllers/main.py
  3. 3
      privacy_consent/data/ir_actions_server.xml
  4. 3
      privacy_consent/data/ir_cron.xml
  5. 66
      privacy_consent/data/mail.xml
  6. 33
      privacy_consent/models/mail_mail.py
  7. 16
      privacy_consent/models/mail_template.py
  8. 60
      privacy_consent/models/privacy_activity.py
  9. 55
      privacy_consent/models/privacy_consent.py
  10. 13
      privacy_consent/models/res_partner.py
  11. 8
      privacy_consent/templates/assets.xml
  12. 7
      privacy_consent/templates/form.xml
  13. 152
      privacy_consent/tests/test_consent.py
  14. 18
      privacy_consent/views/privacy_activity.xml
  15. 13
      privacy_consent/views/privacy_consent.xml
  16. 8
      privacy_consent/views/res_partner.xml
  17. 1
      setup/privacy_consent/odoo/addons/privacy_consent
  18. 6
      setup/privacy_consent/setup.py

4
privacy_consent/__manifest__.py

@ -12,9 +12,7 @@
"license": "AGPL-3", "license": "AGPL-3",
"application": False, "application": False,
"installable": True, "installable": True,
"depends": [
"privacy",
],
"depends": ["privacy"],
"data": [ "data": [
"security/ir.model.access.csv", "security/ir.model.access.csv",
"data/ir_actions_server.xml", "data/ir_actions_server.xml",

28
privacy_consent/controllers/main.py

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

3
privacy_consent/data/ir_actions_server.xml

@ -2,9 +2,7 @@
<!-- Copyright 2018 Tecnativa - Jairo Llopis <!-- Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2019 initOS GmbH - Florian Kantelberg Copyright 2019 initOS GmbH - Florian Kantelberg
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data> <data>
<record id="sync_blacklist" model="ir.actions.server"> <record id="sync_blacklist" model="ir.actions.server">
<field name="name">Sync partner's email blacklist status</field> <field name="name">Sync partner's email blacklist status</field>
<field name="model_id" ref="model_privacy_consent" /> <field name="model_id" ref="model_privacy_consent" />
@ -25,5 +23,4 @@
method(email) method(email)
</field> </field>
</record> </record>
</data> </data>

3
privacy_consent/data/ir_cron.xml

@ -2,9 +2,7 @@
<!-- Copyright 2018 Tecnativa - Jairo Llopis <!-- Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2019 initOS GmbH - Florian Kantelberg Copyright 2019 initOS GmbH - Florian Kantelberg
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data> <data>
<record id="cron_auto_consent" model="ir.cron"> <record id="cron_auto_consent" model="ir.cron">
<field name="name">Request automatic data processing consents</field> <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" />
@ -14,5 +12,4 @@
<field name="interval_type">days</field> <field name="interval_type">days</field>
<field name="numbercall">-1</field> <field name="numbercall">-1</field>
</record> </record>
</data> </data>

66
privacy_consent/data/mail.xml

@ -2,41 +2,54 @@
<!-- Copyright 2018 Tecnativa - Jairo Llopis <!-- Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2019 Tecnativa - Cristina Martin R. Copyright 2019 Tecnativa - Cristina Martin R.
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data> <data>
<!-- Mail templates --> <!-- Mail templates -->
<record id="template_consent" model="mail.template"> <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="name">Personal data processing consent request</field>
<field name="subject">Data processing consent request for ${object.activity_id.display_name|safe}</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="model_id" ref="model_privacy_consent" />
<field name="use_default_to" eval="True" /> <field name="use_default_to" eval="True" />
<field name="lang">${object.partner_id.lang}</field> <field name="lang">${object.partner_id.lang}</field>
<field name="body_html" type="xml"> <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;"> <table style="width:600px;margin:5px auto;">
<tbody> <tbody>
<tr> <tr>
<td> <td>
<a href="/"> <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> </a>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </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> <tbody>
<tr> <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> <p>
Hello, ${object.partner_id.name|safe} Hello, ${object.partner_id.name|safe}
</p> </p>
<p> <p>
We contacted you to ask you to give us your explicit consent to include your data in a data processing activity called 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> </p>
${object.description or ""} ${object.description or ""}
<p> <p>
@ -59,19 +72,32 @@
</td> </td>
</tr> </tr>
<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 Accept
</a> </a>
</td> </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 Reject
</a> </a>
</td> </td>
</tr> </tr>
<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> <p>
If you need further information, please respond to this email and we will attend your request as soon as possible. If you need further information, please respond to this email and we will attend your request as soon as possible.
</p> </p>
@ -88,7 +114,10 @@
<td style="padding-top:10px;font-size: 12px;"> <td style="padding-top:10px;font-size: 12px;">
<p> <p>
Sent by 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> </p>
</td> </td>
</tr> </tr>
@ -97,7 +126,6 @@
</div> </div>
</field> </field>
</record> </record>
<!-- Mail subtypes --> <!-- Mail subtypes -->
<record id="mt_consent_consent_new" model="mail.message.subtype"> <record id="mt_consent_consent_new" model="mail.message.subtype">
<field name="name">New Consent</field> <field name="name">New Consent</field>
@ -123,7 +151,6 @@
<field name="hidden" eval="False" /> <field name="hidden" eval="False" />
<field name="internal" eval="True" /> <field name="internal" eval="True" />
</record> </record>
<record id="mt_activity_consent_new" model="mail.message.subtype"> <record id="mt_activity_consent_new" model="mail.message.subtype">
<field name="name">New Consent</field> <field name="name">New Consent</field>
<field name="description">Privacy consent request created</field> <field name="description">Privacy consent request created</field>
@ -136,7 +163,9 @@
</record> </record>
<record id="mt_activity_acceptance_changed" model="mail.message.subtype"> <record id="mt_activity_acceptance_changed" model="mail.message.subtype">
<field name="name">Acceptance Changed</field> <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="res_model">privacy.activity</field>
<field name="default" eval="True" /> <field name="default" eval="True" />
<field name="hidden" eval="False" /> <field name="hidden" eval="False" />
@ -154,5 +183,4 @@
<field name="parent_id" ref="mt_consent_state_changed" /> <field name="parent_id" ref="mt_consent_state_changed" />
<field name="relation_field">activity_id</field> <field name="relation_field">activity_id</field>
</record> </record>
</data> </data>

33
privacy_consent/models/mail_mail.py

@ -7,8 +7,9 @@ from odoo import models
class MailMail(models.Model): class MailMail(models.Model):
_inherit = "mail.mail" _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.""" """Write consent status after sending message."""
# Know if mail was successfully sent to a privacy consent # Know if mail was successfully sent to a privacy consent
if ( if (
@ -20,17 +21,13 @@ class MailMail(models.Model):
): ):
# Get related consent # Get related consent
consent = self.env["privacy.consent"].browse( 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 # 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( return super()._postprocess_sent_message(
success_pids=success_pids, success_pids=success_pids,
failure_reason=failure_reason, failure_reason=failure_reason,
@ -51,15 +48,11 @@ class MailMail(models.Model):
if self.model != "privacy.consent": if self.model != "privacy.consent":
return result return result
# Tokenize consent links # 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) .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 return result

16
privacy_consent/models/mail_template.py

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

60
privacy_consent/models/privacy_activity.py

@ -7,26 +7,17 @@ from odoo.tools.safe_eval import safe_eval
class PrivacyActivity(models.Model): class PrivacyActivity(models.Model):
_inherit = 'privacy.activity'
_inherit = "privacy.activity"
server_action_id = fields.Many2one( server_action_id = fields.Many2one(
"ir.actions.server", "ir.actions.server",
"Server action", "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 " help="Run this action when a new consent request is created or its "
"acceptance status is updated.", "acceptance status is updated.",
) )
consent_ids = fields.One2many(
"privacy.consent",
"activity_id",
"Consents",
)
consent_count = fields.Integer(
"Consents count",
compute="_compute_consent_count",
)
consent_ids = fields.One2many("privacy.consent", "activity_id", "Consents",)
consent_count = fields.Integer("Consents count", compute="_compute_consent_count",)
consent_required = fields.Selection( consent_required = fields.Selection(
[("auto", "Automatically"), ("manual", "Manually")], [("auto", "Automatically"), ("manual", "Manually")],
"Ask subjects for consent", "Ask subjects for consent",
@ -37,9 +28,7 @@ class PrivacyActivity(models.Model):
"mail.template", "mail.template",
"Email template", "Email template",
default=lambda self: self._default_consent_template_id(), 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. " help="Email to be sent to subjects to ask for consent. "
"A good template should include details about the current " "A good template should include details about the current "
"consent request status, how to change it, and where to " "consent request status, how to change it, and where to "
@ -47,8 +36,7 @@ class PrivacyActivity(models.Model):
) )
default_consent = fields.Boolean( default_consent = fields.Boolean(
"Accepted by default", "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 # Hidden helpers help user design new templates
@ -66,40 +54,43 @@ class PrivacyActivity(models.Model):
@api.depends("consent_ids") @api.depends("consent_ids")
def _compute_consent_count(self): def _compute_consent_count(self):
groups = self.env["privacy.consent"].read_group( 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: 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): def _compute_consent_template_defaults(self):
"""Used in context values, to help users design new templates.""" """Used in context values, to help users design new templates."""
template = self._default_consent_template_id() template = self._default_consent_template_id()
if template: if template:
self.update({
self.update(
{
"consent_template_default_body_html": template.body_html, "consent_template_default_body_html": template.body_html,
"consent_template_default_subject": template.subject, "consent_template_default_subject": template.subject,
})
}
)
@api.constrains("consent_required", "consent_template_id") @api.constrains("consent_required", "consent_template_id")
def _check_auto_consent_has_template(self): def _check_auto_consent_has_template(self):
"""Require a mail template to automate consent requests.""" """Require a mail template to automate consent requests."""
for one in self: for one in self:
if one.consent_required == "auto" and not one.consent_template_id: 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") @api.constrains("consent_required", "subject_find")
def _check_consent_required_subject_find(self): def _check_consent_required_subject_find(self):
for one in self: for one in self:
if one.consent_required and not one.subject_find: if one.consent_required and not one.subject_find:
raise ValidationError(_(
raise ValidationError(
_(
"Require consent is available only for subjects " "Require consent is available only for subjects "
"in current database." "in current database."
))
)
)
@api.model @api.model
def _cron_new_consents(self): def _cron_new_consents(self):
@ -117,19 +108,20 @@ class PrivacyActivity(models.Model):
"""Generate new consent requests.""" """Generate new consent requests."""
consents_vals = [] consents_vals = []
# Skip activitys where consent is not required # 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 = [ domain = [
("id", "not in", one.mapped("consent_ids.partner_id").ids), ("id", "not in", one.mapped("consent_ids.partner_id").ids),
("email", "!=", False), ("email", "!=", False),
] + safe_eval(one.subject_domain) ] + safe_eval(one.subject_domain)
# Store values for creating missing consent requests # Store values for creating missing consent requests
for missing in self.env["res.partner"].search(domain): for missing in self.env["res.partner"].search(domain):
consents_vals.append({
consents_vals.append(
{
"partner_id": missing.id, "partner_id": missing.id,
"accepted": one.default_consent, "accepted": one.default_consent,
"activity_id": one.id, "activity_id": one.id,
})
}
)
# Create and send consent request emails for automatic activitys # Create and send consent request emails for automatic activitys
consents = self.env["privacy.consent"].create(consents_vals) consents = self.env["privacy.consent"].create(consents_vals)
consents.action_auto_ask() consents.action_auto_ask()

55
privacy_consent/models/privacy_consent.py

@ -8,19 +8,19 @@ from odoo import api, fields, models
class PrivacyConsent(models.Model): class PrivacyConsent(models.Model):
_name = 'privacy.consent'
_name = "privacy.consent"
_description = "Consent of data processing" _description = "Consent of data processing"
_inherit = "mail.thread" _inherit = "mail.thread"
_rec_name = "partner_id" _rec_name = "partner_id"
_sql_constraints = [ _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( accepted = fields.Boolean(
track_visibility="onchange", track_visibility="onchange",
help="Indicates current acceptance status, which can come from " help="Indicates current acceptance status, which can come from "
@ -71,18 +71,12 @@ class PrivacyConsent(models.Model):
def _token(self): def _token(self):
"""Secret token to publicly authenticate this record.""" """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( 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( return hmac.new(
secret.encode('utf-8'),
params.encode('utf-8'),
hashlib.sha512,
secret.encode("utf-8"), params.encode("utf-8"), hashlib.sha512,
).hexdigest() ).hexdigest()
def _url(self, accept): def _url(self, accept):
@ -100,9 +94,11 @@ class PrivacyConsent(models.Model):
def _send_consent_notification(self): def _send_consent_notification(self):
"""Send email notification to subject.""" """Send email notification to subject."""
for one in self.with_context(tpl_force_default_to=True,
for one in self.with_context(
tpl_force_default_to=True,
mail_notify_user_signature=False, mail_notify_user_signature=False,
mail_auto_subscribe_no_notify=True):
mail_auto_subscribe_no_notify=True,
):
one.activity_id.consent_template_id.send_mail(one.id) one.activity_id.consent_template_id.send_mail(one.id)
def _run_action(self): def _run_action(self):
@ -112,17 +108,14 @@ class PrivacyConsent(models.Model):
if one.state == "draft": if one.state == "draft":
continue continue
action = one.activity_id.server_action_id.with_context( 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() action.run()
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):
"""Run server action on create.""" """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) results = super_.create(vals_list)
# Sync the default acceptance status # Sync the default acceptance status
results.sudo()._run_action() results.sudo()._run_action()
@ -135,14 +128,11 @@ class PrivacyConsent(models.Model):
return result return result
def message_get_suggested_recipients(self): def message_get_suggested_recipients(self):
result = super() \
.message_get_suggested_recipients()
result = super().message_get_suggested_recipients()
reason = self._fields["partner_id"].string reason = self._fields["partner_id"].string
for one in self: for one in self:
one._message_add_suggested_recipient( one._message_add_suggested_recipient(
result,
partner=one.partner_id,
reason=reason,
result, partner=one.partner_id, reason=reason,
) )
return result return result
@ -168,7 +158,8 @@ class PrivacyConsent(models.Model):
"""Automatically ask for consent.""" """Automatically ask for consent."""
templated = self.filtered("activity_id.consent_template_id") templated = self.filtered("activity_id.consent_template_id")
automated = templated.filtered( automated = templated.filtered(
lambda one: one.activity_id.consent_required == "auto")
lambda one: one.activity_id.consent_required == "auto"
)
automated._send_consent_notification() automated._send_consent_notification()
def action_answer(self, answer, metadata=False): def action_answer(self, answer, metadata=False):
@ -180,8 +171,4 @@ class PrivacyConsent(models.Model):
:param str metadata: :param str metadata:
Metadata from last user acceptance or rejection request. 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" _inherit = "res.partner"
privacy_consent_ids = fields.One2many( privacy_consent_ids = fields.One2many(
"privacy.consent",
"partner_id",
"Privacy consents",
"privacy.consent", "partner_id", "Privacy consents",
) )
privacy_consent_count = fields.Integer( privacy_consent_count = fields.Integer(
"Consents", "Consents",
@ -22,10 +20,9 @@ class ResPartner(models.Model):
def _compute_privacy_consent_count(self): def _compute_privacy_consent_count(self):
"""Count consent requests.""" """Count consent requests."""
groups = self.env["privacy.consent"].read_group( 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: 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"]

8
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 <!-- Copyright 2020 Tecnativa - Jairo Llopis
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data> <data>
<template id="assets_frontend" inherit_id="web.assets_frontend"> <template id="assets_frontend" inherit_id="web.assets_frontend">
<xpath expr="."> <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> </xpath>
</template> </template>
</data> </data>

7
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 <!-- Copyright 2018 Tecnativa - Jairo Llopis
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data> <data>
<template id="form" name="Consent response processed"> <template id="form" name="Consent response processed">
<!-- Use web.login_layout because it gets automatically wrapped <!-- Use web.login_layout because it gets automatically wrapped
by website layout if website is installed, and otherwise includes by website layout if website is installed, and otherwise includes
@ -54,11 +52,12 @@
</p> </p>
<p class="text-muted"> <p class="text-muted">
Sincerely,<br /> Sincerely,<br />
<i t-raw="consent.activity_id.controller_id.with_context(show_address=True, html_format=True).name_get()[0][1]"/>
<i
t-raw="consent.activity_id.controller_id.with_context(show_address=True, html_format=True).name_get()[0][1]"
/>
</p> </p>
</div> </div>
</div> </div>
</t> </t>
</template> </template>
</data> </data>

152
privacy_consent/tests/test_consent.py

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

18
privacy_consent/views/privacy_activity.xml

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

13
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 <!-- Copyright 2018 Tecnativa - Jairo Llopis
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data> <data>
<record model="ir.ui.view" id="consent_form"> <record model="ir.ui.view" id="consent_form">
<field name="name">Privacy Consent Form</field> <field name="name">Privacy Consent Form</field>
<field name="model">privacy.consent</field> <field name="model">privacy.consent</field>
@ -47,7 +45,6 @@
</form> </form>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="consent_tree"> <record model="ir.ui.view" id="consent_tree">
<field name="name">Privacy Consent Tree</field> <field name="name">Privacy Consent Tree</field>
<field name="model">privacy.consent</field> <field name="model">privacy.consent</field>
@ -60,7 +57,6 @@
</tree> </tree>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="consent_search"> <record model="ir.ui.view" id="consent_search">
<field name="name">Privacy Consent Search</field> <field name="name">Privacy Consent Search</field>
<field name="model">privacy.consent</field> <field name="model">privacy.consent</field>
@ -97,17 +93,10 @@
</search> </search>
</field> </field>
</record> </record>
<act_window
id="consent_action"
name="Consents"
res_model="privacy.consent"
/>
<act_window id="consent_action" name="Consents" res_model="privacy.consent" />
<menuitem <menuitem
action="consent_action" action="consent_action"
id="menu_privacy_consent" id="menu_privacy_consent"
parent="privacy.menu_data_protection_master_data" parent="privacy.menu_data_protection_master_data"
/> />
</data> </data>

8
privacy_consent/views/res_partner.xml

@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis <!-- Copyright 2018 Tecnativa - Jairo Llopis
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<data> <data>
<record id="view_partner_form" model="ir.ui.view"> <record id="view_partner_form" model="ir.ui.view">
<field name="name">Add consent smart button</field> <field name="name">Add consent smart button</field>
<field name="model">res.partner</field> <field name="model">res.partner</field>
@ -22,13 +20,9 @@
name="%(consent_action)d" name="%(consent_action)d"
type="action" type="action"
> >
<field
name="privacy_consent_count"
widget="statinfo"
/>
<field name="privacy_consent_count" widget="statinfo" />
</button> </button>
</div> </div>
</field> </field>
</record> </record>
</data> </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