GDPR Compliance¶
Right To Be Forgotten¶
Please see Deleting User Data.
Privacy Records¶
To help with GDPR compliance, we've added a new "privacy records" feature.
There's new template, privacy.html
, you can use to show Europeans and UK residents their
privacy choices on action forms. Our default version displays opt-in/out buttons and a notice, and saves the text of the notice and the option they chose.
We realize groups will apply GDPR differently. We're trying to provide a toolkit, not a prescription for how to comply. We'll cover ways you can override our default behaviors below, and you can build custom setups with templating, custom fields, JavaScript, and the API if our defaults aren't what you want.
We also offer tools to create records when you update user data via uploads, the admin, or the user API.
We'd like to hear via Support if you think you can't do what you need within the current framework.
Getting Started¶
Enable the Privacy Records feature. Go to Configure ActionKit
(under the gear menu at top right), search "Privacy Records" and click Edit
.
Finish reading this section before you do anything else. Checking any of the boxes (like "Require on actions") can break your pages and other functionality. Read the "Getting Started" section and the descriptions of what each checkbox does before checking.
Once you've read everything, enter the countries where you'd like to collect privacy records in the Countries Requiring Privacy Records
box.
Enter one country per line with no commas or semicolons. We use "Europeans," "Europe," etc. in this documentation as shorthand for "countries requiring privacy records," though the UK has kept similar GDPR requirements after leaving the EU, and nothing in ActionKit stops you from adding or removing other countries from the list as you wish.
The built-in templates save the English spelling of country names, even for forms in other langauges, so normally you'll only need to enter those. Here are ActionKit's default names for European countries plus the UK:
Austria
Belgium
Bulgaria
Croatia
Cyprus
Czech Republic
Denmark
Estonia
Finland
France
Germany
Greece
Hungary
Ireland
Italy
Latvia
Lithuania
Luxembourg
Malta
Netherlands
Poland
Portugal
Romania
Slovakia
Slovenia
Spain
Sweden
United Kingdom
Provide required text To provide organization-wide text for checkbox labels,notices, or whatever other details you want to record, use the language editor. On the Pages tab, expand the Language menu in the right-side navigation, then click Languages in the expanded menu (or, if you're viewing these docs from your ActionKit domain, click here), then choose a language. There's no default or placeholder text, so you have to do this step to use the feature!
Scroll to the bottom, click "Translate another piece of text". For the name (the smaller textbox), with our default privacy.html you can use:
privacy_notice
to show Europeans a notice.privacy_optin
andprivacy_optout
to show radio buttons with an opt in/out choice.privacy_really_optout
to show additional content to those who opt out for example, "Please sign up!".
The content (which you type in the longer textbox) may contain HTML like links to policies, but it doesn't need to contain form inputs.
To change or translate the error message when privacy=
is required
but missing, click "Translate another piece of text" again. Use
error_privacy:missing
as the name, and enter whatever text you want to
replace the default message, "We need more information to process your data."
If you don't specify privacy text in every language, text lookup falls back on English to at least display something. Make sure your translations are complete to avoid this!
Changing behavior. While you're in the language admin, you can specify additional "text" to tweak our default behaviors.
If you'd like to unsubscribe current subscribers who choose No, make a
string called privacy_unsub_optouts
and enter any text. Click Save. Remove the text you entered to blank the string out. Click Save. This will turn that off.
If you'd like to show the privacy UI to recognized users with a European
country on file, add a string called privacy_show_if
. Use the value
in_country
to enable it for all Europeans, or missing
to enable it
just for users without an active privacy record. If you don't provide
either, recognized users won't be shown anything.
You only need to set these flags in the English language.
An alternative to configuring the language strings is to edit the privacy.html
template directly. If it's not clear what to change,
look at "New action-processing options" below for more help or ask us via Support.
Adding privacy.html to your templateset. Add {% include
"./privacy.html" %}
to the very end of user_form_wrapper.html
, after the
closing div <div>
tag. Then, if you preview the change on a petition with a
country dropdown and select a European country, you can see the privacy UI.
Donation forms and the user update form at /me/update collect user info
without using user_form_wrapper. For donation pages, you can include the
privacy template under the first <div>
that includes
country_select.html
. If you're using the three-step donation feature,
we've updated function validate_step
and function submit_paypal
in
Original/donate.html to add privacy
to some field lists; validation
still works without the changes, but copying them over will make the UX
smoother when the user forgets to opt in or out.
For the user update screen at /me/update/, a few changes are needed; you
might be able to just copy user_update.html
code from Original to your
templateset, or look for 'privacy' in Original/user_update.html
to see the
relevant parts. Users who opt in there are subscribed to whichever list
you've chosen as your default in the list admin, or list 1 if there's no
default chosen. To reach the list admin, choose the Mailings tab, then
expand the Data menu in the right-hand navigation, then click "Mailing
Lists", or, if viewing these docs on your ActionKit domain, click here.
If your standard opt-in/out doesn't work for /me/update or other particular page types, more options for customization are below.
Viewing the records. Users' privacy records appear in their user records, and action-related records appear in their action history.
If seeing records in the user admin isn't enough, you can use a report to
query the database. The query builder has support for retrieving privacy
records for users and for actions. If you are writing custom SQL, the
database table core_privacyrecord
has records that can be looked up by
user_id
or action_id
. Its version_id
links to the
core_privacytext
table, which contains the text associated with the
record (like notice or opt-in text), the type (like notice
or
optin
), and the hash used as the value for the privacy input. You can't
yet access them through mailing targeting.
There's a lot more to know! You can customize privacy text per page and
page type, revamp privacy.html
to your liking, and more. Even if you're
satisfied with the defaults for now, we suggest at least scanning the
sections below to get an idea how things work.
Customizing Privacy Options on Action Forms¶
Overriding privacy text. You can override privacy text for specific page
types: for example, maybe you want the notice event pages to mention that
hosts see some user info. You could do that in the Language admin by adding
a string named privacy_notice_eventsignup
. The general format is
privacy_[text_type]_[page_type]
. The page type names used must be
lowercase, and are petition, letter, survey, donation (not donate), signup,
call, whipcount, lte, unsubscribe, eventcreate, and eventsignup. For the
user update screen at /me/update/, use update
.
You can override privacy text for individual pages. For that, you add a
custom page field with the same name as the language string, like
privacy_notice
. You'll have to add an "allowed page field" first. To do
that, click the Pages tab, then expand the Other menu in the right-hand
navigation, then click Custom Page Fields, or click here if viewing these docs on your ActionKit
domain.
Custom HTML. You can edit privacy.html
like any other template. Things
to know:
From the browser's side, privacy.html
adds a div with
class="ak-privacy"
. Whenever a European country is picked from the
country dropdown, actionkit.js
unhides .ak-privacy
and enables all of
the inputs under it. When a non-European country is picked (or is the
default when the page loads) ActionKit hides .ak-privacy
and disables
the inputs inside it.
Values of disabled inputs aren't submitted to the server or checked by
validation, so it's much as if .ak-privacy
were removed from the form
entirely when it's hidden and disabled.
You can put whatever you want inside the div. For example, you could add a
custom user field input to the div, and require it using
<input type="hidden" name="required" value="user_xyz">
.
Treating some page types specially. As with any template, you can use
code like {% if page.type == "Donation" %}...{% endif %}
to use
different code with certain page types. You can also use {% if not page.type %}
for code that should apply to the user update screen at /me/update/.
Handling confirmation emails. There are a few approaches to after-action emails. Some organizations have made no changes for European users, treating opt-outs as not subscribing to ongoing updates, but not requiring any change to transactional emails.
Another option is to add text to your transactional or thank you emails reminding opted-out users that this message is a one-off and doesn't mean they'll get more email from you. One way to do that is by adding code to your email wrapper:
{% if not mailing_id %}
{% if user.subscription_status != "subscribed" %}
<p>
<i>We want you to know you're <b>not</b> subscribed to our email
list. This is just a one-off message to thank you for taking action
and offer some next steps.</i>
</p>
{% endif %}
{% endif %}
Or, if you want, you can skip the "thank you" email on actions where the
user opts out. Go to the language admin and add a string called
privacy_optouts_skip_thanks
, with text 1. That doesn't keep the user
from ever receiving thanks emails; it just skips sending the email on the
particular action where they chose to opt out. Keep in mind that suppressing
confirmations could have some odd effects: donors might wonder where their
receipt is, for example.
If you want to make it so that your unsubscribe page doesn't send a
confirmation to recognized European users, you can add HTML to
unsubscribe.html
to do that:
<div class="ak-privacy">
<input type="hidden" name="privacy_show_if" value="in_country">
<input type="hidden" name="skip_confirmation" value="1">
</div>
If you just want to stop transactional emails for a particular user, you can use the Blackholed Emails feature; see Blackholing Email. To get to Blackholing Email, click the Mailings Tab, then expand the List Hygiene menu in the right-hand navigation, then click Blackholed Emails, or click here if viewing these docs on your ActionKit domain.
Custom privacy record types. Our default template creates privacy records of types "notice", "optin", and "optout". Those names aren't magic or hard-coded in the backend, you can add other types.
For example, if you add a language string called privacy_sms_optin
, you
can use {{ privacy_text.sms_optin }}
to access the text from
privacy.html, and submit an input with a value of {{
privacy_text.sms_optin_hash }}
.
"Extra" privacy inputs. If you want to require users pick a "main"
privacy choice when they an action, but also save an extra record (like to
save text of a privacy notice or record an opt-in to text messages), name
your additional input privacy_extra
or privacy_hidden
. That saves another record while making sure that required="privacy"
still only refers to the main choice you're showing the user.
New action-processing options. The default privacy.html
uses some new
action processing options to link opt-ins and opt-outs to subscriptions and
to control whether recognized users see anything. You can use them in your
custom code, too.
An input like <input type="hidden" name="privacy_optin_lists" value="5">
will subscribe the user to list 5 if they submit a privacy= value
of type
'optin'
. The variable {{ list_id }}
is now the page's list ID on action
pages, and the default list ID on /me/update.
An input like <input type="hidden" name="privacy_optout_unsub_all" value="1">
will unsubscribe the user from all lists if they submit a privacy= value
of type 'optout'
.
You can also now submit unsub_all=1
with any action to make it unsubscribe
the user from all lists.
An input like <input type="hidden" name="privacy_show_if" value="in_country">
shows the privacy UI if a recognized user is in a European country. A value
of missing
only shows the UI if there's no existing record (of any type:
optin, optout, or something else) on file.
Using standard country names is important. The system currently doesn't recognize that, for example, "Deutschland" means "Germany", so if you aren't consistent in the country names you use, users might not be recognized as Europeans. This is especially key in the user admin, where there's no dropdown to push users to use the correct values.
JavaScript hook. You can handle a jQuery event when .ak-privacy
is
shown or hidden. After initForm has been called, use
$(actionkit.form).on('actionkit.privacyRequirementChanged', handler)
to
add a handler. The handler will be passed an event object and the new
privacy-required status.
You must use resources/actionkit.js. Code to show and hide the
.ak-privacy
div is in an update to resources/actionkit.js
. If you're
loading your JavaScript from any other location, including custom forks of
actionkit.js
(we don't recommend forking!), our code to hide and disable the
privacy UI won't run. Updates to the feature will require you have the
latest actionkit.js
too.
FYI: Users with disabled or broken JavaScript are treated like
Europeans. Since we rely on JavaScript to hide the privacy UI when it's
not needed, it's inevitable that this won't work quite right if JS is broken
or disabled. To try to err on the safe side, .ak-privacy
is visible and
enabled if the JS is missing or doesn't run.
Non-Action User Updates¶
User info doesn't always come through actions: import pages, the REST user API, and the user admin can also make changes. We've added ways you can create (and, if you want, require) privacy records for those updates too.
When editing a user profile or import page, you'll see a "Privacy notes" field once you've configured a set of countries. By default, there's a dropdown of options you can edit. Under the dropdown you have links to edit the choices in the dropdown or enter one-off custom privacy notes. The type for privacy records from import pages is 'import_notes' and the type for user admin updates is 'admin_notes'. The user admin's subscription features don't create privacy records, and the form to subscribe a user through the admin is disabled for Europeans. (You can subscribe the user through a special action page as a workaround.)
On the user admin page, you'll only see the "Privacy notes" dropdown when you're going to save a European country for the user. On import pages, the dropdown's always visible, and records are saved even for non-Europeans. (If those extra records cause you problems, contact us via Support.)
If you want to bulk-add records with a different type from 'import_notes', you can also provide 'privacy' as a column in an upload, and set its value to the same hash value that's submitted from an action form (not raw text).
In the user API, you can now submit a privacy= value with PUT or POST requests. The value can either be a hash like the ones submitted with actions, or other text that will be saved like custom notes. The type for these is always 'api_notes'. To encourage third-party integrators to work with you on correct privacy compliance rather than just submitting something like privacy=1 to try to get past the error, we'll reject "1", "true", and a handful of other values.
Also, via the Config screen you can configure privacy records as required in any or all of these contexts:
- Requiring notes in the user admin will require staff to pick privacy notes before saving any updates to the user's profile, if the user's country after the changes is European.
- Requiring notes on import pages for uploads through the API and admin. Old import pages will no longer work until you specify notes for them, and any integrations that don't set privacy notes will break. It requires notes on all import pages, since any import page could update European users.
- Requiring notes on the user API will break any integrations unprepared to pass
privacy=
with any updates that either include a European country or don't include country but affect a user with a European country already assigned. It also disables the legacy XML-RPC User API, which doesn't support the new feature.
We're not advising you to use these "Require on..." features or not. You can still save privacy records on some imports, for example, without requiring them everywhere.
Requiring Records on All Actions¶
Checking the "Require on actions" box requires a privacy record with every
action submitted with a European value for country=
, excluding import
and unsubscribe pages, and including actions submitted through the API. It
also requires privacy values on the profile update form at /me/update/.
We're not advising you to use this feature or not. When our default privacy UI is visible on an action page, it already requires the user to make a choice. What "Require on actions" changes is that if some user doesn't see the privacy UI after choosing a European country from a dropdown (maybe because of a bug or an outdated form), we won't accept their action.
If you want to use this, remember it will intentionally break actions from sources that don't collect privacy records. It can break actions even when they are coming in through third-party integrations, through old forms hosted on third-party sites that you can't feasibly update, or through forms that don't subscribe users. If you enable this option and it rejects actions you wanted to be accepted, we won't be able to recover the rejected data, because rejecting the actions was intentional behavior.
If you believe you need to take that risk, we still suggest that, if
feasible, you don't flip the switch immediately. Instead, update your forms
without enabling "Require on actions", then look for any submits missing
records by querying for actions where created_user=1
and the user's
country is in Europe, but there is no privacy record. That can reveal
straggler forms that need updating.
Some things to remember about this feature:
- This only requires records when a European country is submitted. If a user has a European country previously stored but no country submitted with the action (maybe because the form only asks for email, or because the user's recognized) that doesn't trigger the requirement.
- It requires only one record, which has to come in with the name
privacy
(notprivacy_hidden
orprivacy_extra
).
Marking Records as Withdrawn¶
Privacy records have a status
column that's initially active
when a
record is created. By default, when a user unsubscribes from all lists, all
of their records are marked status="withdrawn"
. That includes opt-outs
through unsubscribe pages, through the user admin, or through choosing the
opt-out radio button if you've enabled the privacy_unsub_optouts
option.
Unsubscribes through bounce processing and reengagement don't mark records withdrawn, because they weren't requested by the user.
You'll see the withdrawn status in the lists of privacy records in the user admin and action history.
The default behavior is meant to cover the common situation of using privacy
records for email opt-ins, but if you want to customize things, the new
withdraw_privacy
action-processing option may help.
If you don't want an unsubscribe to withdraw privacy records, submit
withdraw_privacy=no_auto
. This might make sense if you're tracking
something like SMS opt-ins and you want them to be independent of email
opt-ins. If you want an action to withdraw all privacy records when it
wouldn't by default, you can submit withdraw_privacy=all
.
If you want to withdraw only certain records, you can submit
withdraw_privacy=[hash]
. To get the hashes for existing records from a
Django template, you can use a for loop like {% for record in
user.privacyrecord_set.all %}
and use record.version.type
,
record.version.status
, and record.version.hash
to filter down to
records of interest and get the hashes you need to submit.