Ensuring Privacy Compliance - Popup Consent Implementation for User Traffic Tracking

Introduction

I am expanding my portfolio site to track traffic and gain insights into visitor behavior. However, I understand the importance of privacy compliance and want to ensure that I adhere to local and global privacy laws. To achieve this, I have identified three key objectives for my site:

  1. Right to Know: I will inform users about the data I collect through my privacy policy. When users accept the popup, their consent will be linked to that version of the policy. If any changes occur, users will need to consent to the updated policy.

  2. Right to Delete: I provide users with the option to request the deletion of their data. They can easily contact me via email to make such requests.

  3. Right to Opt-Out: By default, users are opted out unless they consent to the popup. If users clear their cookies or choose "No Thanks" on the popup, they will be opted out. I will clarify this in the revised policy, as I am still working on the wording and no tracking is currently in place.

Consent as Defined by GDPR Art 7

According to GDPR Art 7, when processing personal data based on consent, the controller (me) must be able to demonstrate that the data subject (user) has given their consent. The request for consent should be presented clearly and separately from other matters, using easily understandable language. The data subject has the right to withdraw their consent at any time, without affecting the lawfulness of processing prior to the withdrawal. The data subject must be informed about their right to withdraw consent, and the process of withdrawal should be as easy as giving consent.

When assessing whether consent is freely given, it is important to consider if the performance of a contract or service is conditional on consent for processing personal data that is not necessary for the performance of that contract.

CDPA Chapter 53

According to CDPA Chapter 53, "consent" refers to a clear affirmative act that signifies a consumer's freely given, specific, informed, and unambiguous agreement to process their personal data. Consent can be provided through a written statement, including electronic means, or any other unambiguous affirmative action.

Design

For my portfolio project, I aimed to ensure compliance with relevant privacy laws. To achieve this, I focused on two main objectives:

  1. Versioned Privacy Policy and Terms of Use
  2. User Consent Mechanism

Versioning Privacy Policy and Terms of Use

To begin with, I transformed my existing static Privacy Policy and Terms of Service pages into dynamic database entries, allowing for easy version control.

Privacy Policy Model

I crafted a Django model specifically for the Privacy Policy:

class PrivacyPolicy(models.Model):
    published_date = models.DateTimeField(auto_now_add=True)
    intro = models.TextField()
    policy = models.TextField()

    def __str__(self):
        return f"Privacy Policy published on {self.published_date}"

Terms of Use Model

Similarly, I created a model for the Terms of Use:

class TermsOfUse(models.Model):
    published_date = models.DateTimeField(auto_now_add=True)
    intro = models.TextField()
    policy = models.TextField()

    def __str__(self):
        return f"Terms of Use published on {self.published_date}"

Admin Interface Setup

For convenient management, I registered both models in the Django admin interface:

class TermsOfUseAdmin(admin.ModelAdmin):
    list_display = ("published_date", "intro", "policy")
    search_fields = ("published_date",)

class PrivacyPolicyAdmin(admin.ModelAdmin):
    list_display = ("published_date", "intro", "policy")
    search_fields = ("published_date",)

admin.site.register(TermsOfUse, TermsOfUseAdmin)
admin.site.register(PrivacyPolicy, PrivacyPolicyAdmin)

By implementing these models and setting up the admin interface, I can easily maintain and update the Privacy Policy and Terms of Use to ensure ongoing compliance with privacy regulations.

This approach not only enhances transparency for users but also establishes a robust foundation for managing legal requirements within my portfolio site. All that is left to do is convert my django view for both the Terms of Use and Privacy Policy to display the latest versions of each and convert the markdown to html.

Here is the view for the Privacy Policy:

def privacy_policy(request):

    # Fetch The Privacy Policy
    privacy_policy = PrivacyPolicy.objects.latest("published_date")

    # The Current Privacy Policy Intro
    privacy_policy_intro = privacy_policy.intro

    # The Privacy Policy Publish Date
    privacy_policy_publish_date = privacy_policy.published_date

    # The Policy In Markdown
    privacy_policy_markdown = markdown.markdown(
        privacy_policy.policy, extensions=extensions
    )

    return render(
        request,
        "privacy_policy.html",
        {
            "privacy_policy_intro": privacy_policy_intro,
            "privacy_policy_publish_date": privacy_policy_publish_date,
            "privacy_policy_markdown": privacy_policy_markdown,
        },
    )

Here is the view for the Terms of Use:

def terms(request):

    # Fetch The Terms Of Use
    terms_of_use = TermsOfUse.objects.latest("published_date")

    # The Current Terms Of Use Intro
    terms_of_use_intro = terms_of_use.intro

    # The Terms Of Use Publish Date
    terms_of_use_publish_date = terms_of_use.published_date

    # The Policy In Markdown
    terms_of_use_markdown = markdown.markdown(
        terms_of_use.policy, extensions=extensions
    )
    return render(
        request,
        "terms.html",
        {
            "terms_of_use_intro": terms_of_use_intro,
            "terms_of_use_publish_date": terms_of_use_publish_date,
            "terms_of_use_markdown": terms_of_use_markdown,
        },
    )

Policy Consent Popup

Moving forward, I focused on designing a visually appealing popup that meets our functional requirements. Initially, I concentrated on the frontend implementation and client-side interactions before addressing the backend functionalities.

<div class='cookie-banner' id='cookie-banner' style="display: none;">
        <div class="container">
            <div class="row align-items-center">
                <div class="col-sm-12 col-md-7 col-lg-7">
                    <h3>I Value Your Privacy</h3>
                    <p>I use cookies and similar technologies to enhance your browsing experience, serve personalized content, and analyze traffic patterns. Your privacy is important to me, and I want to ensure you have a choice.</p>
                    <p>By clicking "I Accept", you consent to cookie usage and features disclosed in the <a href = "{% url 'privacy_policy' %}">Privacy Policy</a></p>
                </div>
                <div class="col-sm-12 col-md-5 col-lg-5">
                    <center>
                        <form id="policyConsentForm">
                            {% csrf_token %}
                            {{ policy_consent_form.as_p }}
                            <button type="button" class="btn btn-outline-secondary" onclick="declineCookies()">No Thanks</button>
                            <button type="submit" class="btn btn-success" onclick="acceptCookies()">I Accept</button>
                        </form>
                    </center>
                </div>
            </div>
        </div>
    </div>

This design emphasizes clarity and user choice. It informs visitors about cookie usage and provides a direct link to the Privacy Policy for further details. The form allows users to either accept or decline cookie usage, ensuring compliance with privacy regulations while maintaining a user-friendly interface.

Next, we will add CSS to style the popup banner and optimize it for various screen sizes. We will also utilize built-in animations to animate the entrance of the popup onto the screen.

Here is the CSS code:

.cookie-banner {
    position: fixed;
    bottom: 0px;
    width: 100%;
    height: 160px;
    padding: 5px 14px;
    background-color: #f2f4f5;
    border-radius: 0px;
    box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.2);
    z-index: 2;
    animation-name: TrackingNotice;
    animation-duration: 1s;
    animation-timing-function: ease-in-out;
}

@keyframes TrackingNotice {
    0%   {height:0px;}
    100% {height:160px;}
}

@media (max-width: 241px) {
    .cookie-banner {
        height: 550px;
    }
    @keyframes TrackingNotice {
        0%   {height:0px;}
        100% {height:550px;}
    }
}

@media (max-width: 361px) and (min-width: 242px) {
    .cookie-banner {
        height: 420px;
    }
    @keyframes TrackingNotice {
        0%   {height:0px;}
        100% {height:420px;}
    }
}

@media (max-width: 429px) and (min-width: 360px) {
    .cookie-banner {
        height: 320px;
    }
    @keyframes TrackingNotice {
        0%   {height:0px;}
        100% {height:320px;}
    }
}

@media (max-width: 665px) and (min-width: 430px) {
    .cookie-banner {
        height: 250px;
    }
    @keyframes TrackingNotice {
        0%   {height:0px;}
        100% {height:250px;}
    }
}

@media (max-width: 949px) and (min-width: 666px) {
    .cookie-banner {
        height: 230px;
    }
    @keyframes TrackingNotice {
        0%   {height:0px;}
        100% {height:230px;}
    }
}

@media (max-width: 1023px) and (min-width: 950px) {
    .cookie-banner {
        height: 190px;
    }
    @keyframes TrackingNotice {
        0%   {height:0px;}
        100% {height:190px;}
    }
}

@media (max-width: 1180px) and (min-width: 1024px) {
    .cookie-banner {
        height: 190px;
    }
    @keyframes TrackingNotice {
        0%   {height:0px;}
        100% {height:190px;}
    }
}

@media (max-width: 1366px) and (min-width: 1181px) {
    .cookie-banner {
        height: 190px;
    }
    @keyframes TrackingNotice {
        0%   {height:0px;}
        100% {height:190px;}
    }
}

This CSS code defines the styling for the popup banner. The .cookie-banner class sets the position, width, height, padding, background color, border radius, and box shadow properties. It also includes an animation called TrackingNotice that animates the height of the banner from 0px to 160px.

The @media queries are used to apply different styles based on the screen size. For example, when the screen width is less than or equal to 241px, the height of the banner is set to 550px. Similar adjustments are made for other screen sizes using different @media queries.

Policy Consent Backend

In order to manage user consent effectively for the privacy policy on my portfolio site, I've implemented the following backend components:

PolicyConsent Model

class PolicyConsent(models.Model):
    privacy_policy = models.ForeignKey(PrivacyPolicy, on_delete=models.CASCADE)
    ip_address = models.GenericIPAddressField()
    date_accepted = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"Privacy Policy consent accepted on {self.date_accepted}"

Explanation:

  • PolicyConsent Model: This model records instances where users accept the privacy policy. It links to a specific PrivacyPolicy through a foreign key relationship, captures the user's IP address, and records the date and time when consent was given.

PolicyConsentAdmin Configuration

class PolicyConsentAdmin(admin.ModelAdmin):
    list_display = ("privacy_policy", "ip_address", "date_accepted")
    search_fields = ("privacy_policy", "ip_address")

admin.site.register(PolicyConsent, PolicyConsentAdmin)

Explanation:

  • PolicyConsentAdmin: Configures the Django admin interface to display and search PolicyConsent instances by privacy policy, IP address, and date accepted, facilitating easy management and monitoring of user consents.

PolicyConsentForm

class PolicyConsentForm(forms.ModelForm):
    privacy_policy = forms.ModelChoiceField(
        queryset=PrivacyPolicy.objects.all(), widget=forms.HiddenInput(), required=False
    )
    ip_address = forms.GenericIPAddressField(widget=forms.HiddenInput(), required=False)

    class Meta:
        model = PolicyConsent
        fields = ["privacy_policy", "ip_address"]

Explanation:

  • PolicyConsentForm: Defines a form using Django's ModelForm to handle user consent submissions. It includes fields for privacy_policy (hidden input) and ip_address (hidden input), ensuring the privacy policy version and user's IP address are captured upon submission.

policy_consent_ajax View

def policy_consent_ajax(request):
    if request.method == "POST":
        form = PolicyConsentForm(request.POST)
        if form.is_valid():
            try:
                latest_policy = PrivacyPolicy.objects.latest("published_date")
            except PrivacyPolicy.DoesNotExist:
                print("No PrivacyPolicy exists")
                return JsonResponse({"success": False})

            consent = form.save(commit=False)
            consent.ip_address = form.cleaned_data.get("ip_address")
            consent.privacy_policy = latest_policy
            consent.save()

            return JsonResponse({"success": True})
        else:
            print("Form is not valid")
            print(form.errors)
    else:
        print("Request is not POST")

    return JsonResponse({"success": False})

Explanation:

  • policy_consent_ajax View: Handles AJAX requests for submitting policy consents. It validates the form data submitted, retrieves the latest privacy policy, creates a PolicyConsent instance with the user's IP address and the latest policy version, and saves it to the database. Returns a JSON response indicating success or failure.

PrivacyPolicyApiView and TermsApiView

class PrivacyPolicyApiView(APIView):
    def get(self, request, *args, **kwargs):
        try:
            privacy_policy = PrivacyPolicy.objects.latest("published_date")
        except PrivacyPolicy.DoesNotExist:
            raise NotFound("No privacy policy available")

        serializer = PrivacyPolicySerializer(privacy_policy)
        return Response(serializer.data)

class TermsApiView(APIView):
    def get(self, request, *args, **kwargs):
        try:
            terms_of_use = TermsOfUse.objects.latest("published_date")
        except TermsOfUse.DoesNotExist:
            raise NotFound("No terms of use policy available")

        serializer = TermsOfUseSerializer(terms_of_use)
        return Response(serializer.data)

Explanation:

  • PrivacyPolicyApiView and TermsApiView: Provide API endpoints (GET methods) to fetch the latest privacy policy and terms of use respectively. These views fetch the latest versions of PrivacyPolicy and TermsOfUse models, serialize the data using PrivacyPolicySerializer and TermsOfUseSerializer, and return the serialized data in the API response format.

PrivacyPolicySerializer and TermsOfUseSerializer

class PrivacyPolicySerializer(serializers.ModelSerializer):
    class Meta:
        model = PrivacyPolicy
        fields = ["published_date", "intro", "policy"]

class TermsOfUseSerializer(serializers.ModelSerializer):
    class Meta:
        model = TermsOfUse
        fields = ["published_date", "intro", "policy"]

Explanation:

  • PrivacyPolicySerializer and TermsOfUseSerializer: Serialize PrivacyPolicy and TermsOfUse models to JSON format for API responses. These serializers specify which fields to include (published_date, intro, policy) from their respective models.

Privacy Policy Consent JS

The JavaScript code provided is responsible for implementing the privacy policy consent functionality.

function setCookie(name, value, days) {
    var expires = "";
    if (days) {
        var date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
        expires = "; expires=" + date.toUTCString();
    }
    document.cookie = name + "=" + (value || "") + expires + "; path=/";
}

function getCookie(name) {
    var nameEQ = name + "=";
    var ca = document.cookie.split(';');
    for(var i=0; i< ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
    }
    return null;
}

function checkCookie() {
    getPrivacyPolicyDate().then(date => {
        var cookieName = "acceptedSitePolicy" + date;
        var user = getCookie(cookieName);

        if (!user) {
            document.getElementById("cookie-banner").style.display = "block";
        }
    });
}

function acceptCookies() {
    getPrivacyPolicyDate().then(date => {
        var cookieName = "acceptedSitePolicy" + date;
        setCookie(cookieName, "yes", 365);
    });

    document.getElementById("cookie-banner").style.display = "none";

    document.getElementById('policyConsentForm').addEventListener('submit', function(event) {
        event.preventDefault();
        var form = this;
        var formData = new FormData(form);

        getClientIP().then(ip => {
            formData.append('ip_address', ip);
            fetch(djangoData.policyConsentAjaxUrl, {
                method: 'POST',
                headers: {
                    'X-CSRFToken': formData.get('csrfmiddlewaretoken')
                },
                body: formData
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    // Handle success
                    console.log('Consent recorded successfully.');
                } else {
                    // Handle error
                    console.log('Failed to record consent.');
                }
            })
            .catch(error => {
                console.error('Error:', error);
            });
        });
    });
}

function declineCookies() {
    getPrivacyPolicyDate().then(date => {
        var cookieName = "acceptedSitePolicy" + date;
        setCookie(cookieName, "no", 365);
    });

    document.getElementById("cookie-banner").style.display = "none";
}

function getClientIP() {
    return fetch('https://api.ipify.org?format=json')
        .then(response => response.json())
        .then(data => {
            return data.ip;
        })
        .catch(error => {
            console.error('Error:', error);
        });
}

function getPrivacyPolicyDate() {
    return fetch(djangoData.privacyPolicyApiUrl)
        .then(response => response.json())
        .then(data => {
            var date = new Date(data.published_date);
            var year = date.getFullYear();
            var month = ("0" + (date.getMonth() + 1)).slice(-2); 
            var day = ("0" + date.getDate()).slice(-2);
            var formattedDate = year + month + day;
            return formattedDate;
        })
        .catch(error => {
            console.error('Error:', error);
        });
}

window.onload = checkCookie;
  • The setCookie function is used to set a cookie with a given name, value, and expiration time in days. It uses the document.cookie property to set the cookie.
function setCookie(name, value, days) {
    var expires = "";
    if (days) {
        var date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
        expires = "; expires=" + date.toUTCString();
    }
    document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
  • The getCookie function is used to retrieve the value of a cookie with a given name. It parses the document.cookie property and searches for the cookie with the specified name.
function getCookie(name) {
    var nameEQ = name + "=";
    var ca = document.cookie.split(';');
    for(var i=0; i< ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
    }
    return null;
}
  • The checkCookie function is called when the page loads. It retrieves the privacy policy date using the getPrivacyPolicyDate function. It then constructs a cookie name based on the policy date and checks if the user has accepted the policy by checking the corresponding cookie value. If the user has not accepted the policy, it displays a cookie banner by setting the display property of the element with the ID "cookie-banner" to "block".
function checkCookie() {
    getPrivacyPolicyDate().then(date => {
        var cookieName = "acceptedSitePolicy" + date;
        var user = getCookie(cookieName);

        if (!user) {
            document.getElementById("cookie-banner").style.display = "block";
        }
    });
}
  • The acceptCookies function is called when the user accepts the privacy policy. It retrieves the privacy policy date using the getPrivacyPolicyDate function. It constructs a cookie name based on the policy date and sets the cookie value to "yes" with an expiration time of 365 days using the setCookie function. It then hides the cookie banner by setting the display property of the element with the ID "cookie-banner" to "none". Additionally, it attaches an event listener to the form with the ID "policyConsentForm" to handle form submission. When the form is submitted, it prevents the default form submission behavior, retrieves the form data using FormData, appends the client IP address obtained from the getClientIP function to the form data, and sends a POST request to the djangoData.policyConsentAjaxUrl endpoint with the form data and CSRF token in the headers. The response is then parsed as JSON, and based on the success status, a success or error message is logged to the console.
function acceptCookies() {
    getPrivacyPolicyDate().then(date => {
        var cookieName = "acceptedSitePolicy" + date;
        setCookie(cookieName, "yes", 365);
    });

    document.getElementById("cookie-banner").style.display = "none";

    document.getElementById('policyConsentForm').addEventListener('submit', function(event) {
        event.preventDefault();
        var form = this;
        var formData = new FormData(form);

        getClientIP().then(ip => {
            formData.append('ip_address', ip);
            fetch(djangoData.policyConsentAjaxUrl, {
                method: 'POST',
                headers: {
                    'X-CSRFToken': formData.get('csrfmiddlewaretoken')
                },
                body: formData
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    // Handle success
                    console.log('Consent recorded successfully.');
                } else {
                    // Handle error
                    console.log('Failed to record consent.');
                }
            })
            .catch(error => {
                console.error('Error:', error);
            });
        });
    });
}
  • The declineCookies function is called when the user declines the privacy policy. It retrieves the privacy policy date using the getPrivacyPolicyDate function. It constructs a cookie name based on the policy date and sets the cookie value to "no" with an expiration time of 365 days using the setCookie function. It then hides the cookie banner by setting the display property of the element with the ID "cookie-banner" to "none".
function declineCookies() {
    getPrivacyPolicyDate().then(date => {
        var cookieName = "acceptedSitePolicy" + date;
        setCookie(cookieName, "no", 365);
    });

    document.getElementById("cookie-banner").style.display = "none";
}
  • The getClientIP function makes a GET request to the "https://api.ipify.org?format=json" endpoint to retrieve the client's IP address. The response is parsed as JSON, and the IP address is returned.
function getClientIP() {
    return fetch('https://api.ipify.org?format=json')
        .then(response => response.json())
        .then(data => {
            return data.ip;
        })
        .catch(error => {
            console.error('Error:', error);
        });
}
  • The getPrivacyPolicyDate function makes a GET request to the djangoData.privacyPolicyApiUrl endpoint to retrieve the privacy policy data. The response is parsed as JSON, and the published date is extracted. The date is then formatted as "YYYYMMDD" and returned.
function getPrivacyPolicyDate() {
    return fetch(djangoData.privacyPolicyApiUrl)
        .then(response => response.json())
        .then(data => {
            var date = new Date(data.published_date);
            var year = date.getFullYear();
            var month = ("0" + (date.getMonth() + 1)).slice(-2); 
            var day = ("0" + date.getDate()).slice(-2);
            var formattedDate = year + month + day;
            return formattedDate;
        })
        .catch(error => {
            console.error('Error:', error);
        });
}
  • Finally, the window.onload event handler is set to call the checkCookie function when the page finishes loading.
window.onload = checkCookie;

In summary, this JavaScript code sets and retrieves cookies, checks if the user has accepted the privacy policy, handles cookie acceptance and decline actions, retrieves the client's IP address, and retrieves the privacy policy date. It also includes functionality to send a form submission with additional data to a server endpoint.

Sources: