diff --git a/privacy_consent/__manifest__.py b/privacy_consent/__manifest__.py index c4e84ef..71abc69 100644 --- a/privacy_consent/__manifest__.py +++ b/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", diff --git a/privacy_consent/controllers/main.py b/privacy_consent/controllers/main.py index dddb20b..60bd233 100644 --- a/privacy_consent/controllers/main.py +++ b/privacy_consent/controllers/main.py @@ -11,9 +11,12 @@ from odoo.addons.web.controllers.main import ensure_db class ConsentController(Controller): - @route("/privacy/consent//" - "/", - type="http", auth="none", website=True) + @route( + "/privacy/consent//" "/", + 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(), diff --git a/privacy_consent/data/ir_actions_server.xml b/privacy_consent/data/ir_actions_server.xml index d305284..149cbfa 100644 --- a/privacy_consent/data/ir_actions_server.xml +++ b/privacy_consent/data/ir_actions_server.xml @@ -1,14 +1,12 @@ - + - - Sync partner's email blacklist status - - + + code for consent in records: @@ -25,5 +23,4 @@ method(email) - diff --git a/privacy_consent/data/ir_cron.xml b/privacy_consent/data/ir_cron.xml index 72acc37..d008b86 100644 --- a/privacy_consent/data/ir_cron.xml +++ b/privacy_consent/data/ir_cron.xml @@ -1,18 +1,15 @@ - + - - Request automatic data processing consents - + code model._cron_new_consents() 1 days -1 - diff --git a/privacy_consent/data/mail.xml b/privacy_consent/data/mail.xml index f4ab84e..a485970 100644 --- a/privacy_consent/data/mail.xml +++ b/privacy_consent/data/mail.xml @@ -1,42 +1,55 @@ - + - - - + Personal data processing consent request - Data processing consent request for ${object.activity_id.display_name|safe} - - + Data processing consent request for ${object.activity_id.display_name|safe} + + ${object.partner_id.lang} -
+
- ${object.activity_id.controller_id.display_name|safe} + ${object.activity_id.controller_id.display_name|safe}
- +
- - - - @@ -97,62 +126,61 @@ - New Consent Privacy consent request created privacy.consent - - - + + + Acceptance Changed by Subject Acceptance status updated by subject privacy.consent - - - + + + State Changed Privacy consent request state changed privacy.consent - - - + + + - New Consent Privacy consent request created privacy.activity - - - - + + + + activity_id Acceptance Changed - Privacy consent request acceptance status changed + Privacy consent request acceptance status changed privacy.activity - - - - + + + + activity_id State Changed Privacy consent request state changed privacy.activity - - - - + + + + activity_id - diff --git a/privacy_consent/models/mail_mail.py b/privacy_consent/models/mail_mail.py index 4c1f09d..5a94f5d 100644 --- a/privacy_consent/models/mail_mail.py +++ b/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 diff --git a/privacy_consent/models/mail_template.py b/privacy_consent/models/mail_template.py index 9e8f786..207d216 100644 --- a/privacy_consent/models/mail_template.py +++ b/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" - 'Accept\n' - 'Reject' - ) % ( - "/privacy/consent/accept/", - "/privacy/consent/reject/", - )) + raise ValidationError( + _( + "Missing privacy consent link placeholders. " + "You need at least these two links:\n" + 'Accept\n' + 'Reject' + ) + % ("/privacy/consent/accept/", "/privacy/consent/reject/",) + ) diff --git a/privacy_consent/models/privacy_activity.py b/privacy_consent/models/privacy_activity.py index 019ac3c..cf01ebd 100644 --- a/privacy_consent/models/privacy_activity.py +++ b/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() diff --git a/privacy_consent/models/privacy_consent.py b/privacy_consent/models/privacy_consent.py index 1722ba8..19e6688 100644 --- a/privacy_consent/models/privacy_consent.py +++ b/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}) diff --git a/privacy_consent/models/res_partner.py b/privacy_consent/models/res_partner.py index 1c894a7..6961c92 100644 --- a/privacy_consent/models/res_partner.py +++ b/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"] diff --git a/privacy_consent/templates/assets.xml b/privacy_consent/templates/assets.xml index 27e7436..de1c77f 100644 --- a/privacy_consent/templates/assets.xml +++ b/privacy_consent/templates/assets.xml @@ -1,13 +1,13 @@ - + - - - diff --git a/privacy_consent/templates/form.xml b/privacy_consent/templates/form.xml index 7d241ca..9bacb35 100644 --- a/privacy_consent/templates/form.xml +++ b/privacy_consent/templates/form.xml @@ -1,9 +1,7 @@ - + - -
+

Hello, ${object.partner_id.name|safe}

We contacted you to ask you to give us your explicit consent to include your data in a data processing activity called - ${object.activity_id.display_name|safe}, property of - ${object.activity_id.controller_id.display_name|safe} + ${object.activity_id.display_name|safe}, property of + ${object.activity_id.controller_id.display_name|safe}

${object.description or ""}

@@ -59,19 +72,32 @@

- + + Accept - + + Reject
+

If you need further information, please respond to this email and we will attend your request as soon as possible.

@@ -88,7 +114,10 @@

Sent by - ${object.activity_id.controller_id.display_name|safe}. + ${object.activity_id.controller_id.display_name|safe}.