1

I have the following (simplified) Model:

class Order(models.Model):
    is_anonymized = models.BooleanField(default=False)
    billing_address = models.ForeignKey('order.BillingAddress', null=True, blank=True)

I want to hide the billing_address for objects where the customer chosen to do so by setting is_anonymized=True.

My best approach so far was to do that in the init:

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.is_anonymized:
            self.billing_address = None
            self.billing_address_id = None

This works fine in the Admin BUT...

anywhere in the code there are select_related-QuerySets for Orders:

    queryset = Order._default_manager.select_related('billing_address')

All places where the select_related-querysets are used, the billing_address is accidentally shown. Elsewhere (like in the admin), it isn't.

How can I ensure to remove the billing_address everywhere for objects with is_anonymized = True?

I thought about overwriting the queryset in the manager but i couldn't overwrite the billing_address field by condition.

Using the getter-setter pattern was not a good solution because it breaks the admin at multiple places (there are many attributes to cloak like billing_address).

To be more precise: The goal is to only simulate as if the data would be deleted although persisting in the database.

Frank
  • 1,959
  • 12
  • 27

1 Answers1

1

I would like to start by saying that I do not understand why would you want to hide information from the admin of your system. Unless you have a complex work environment where only the DBA have access to such information, I honestly do not see the point.

To answer your question...

To hide information in the admin page, one option is to disable all links and replace the HTML with the edit link when is_anonymized value is False: (adapted from answer_1 and answer_2)

admin.py:

from django.utils.html import format_html

class OrderAdmin(admin.ModelAdmin):
    list_display = ['anonymous_address']

    def anonymous_address(self, obj):
        if not obj.is_anonymized:
            return format_html(u'<a href="/admin/app/order/{}/change/">{}</a>', obj.id, obj.billing_address.address)
        else:
            return ("%s" % ('anonymous'))
    
    def __init__(self, *args, **kwargs):
        super(OrderAdmin, self).__init__(*args, **kwargs)
        self.list_display_links = None

admin.site.register(Order, OrderAdmin)

Note that with this solution admin still has access to BillingAddress model, if you registered it in the admin site. In that case it will be also necessary to override that.

On your queries, you can aggregate values with conditional expressions:

views.py:

from core.models import Order
from django.db.models import When, Case

def anonymous_address(request):
    orders = Order.objects.annotate(anonymised_address=Case(
        When(is_anonymized=True, then=None),
        When(is_anonymized=False, then='billing_address'),
    )).values('is_anonymized', 'anonymised_address')
    
    context = {'orders': orders}
    return render(request, 'anonymous_address.html', context)

anonymous_address.html:


{% block content %}
    {% for order in orders %}
        Should be anonymous: {{order.is_anonymized}} <br>
        Address: {{order.anonymised_address}}
        <hr>
    {% endfor %}
{% endblock content %}

And, instead of having this long query in every view, it is possible to replace that by a custom manager:

models.py:

class AnonymousOrdersManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().annotate(anonymised_address=Case(
                    When(is_anonymized=True, then=None),
                    When(is_anonymized=False, then='billing_address'),
                )).values('is_anonymized', 'anonymised_address')

class Order(models.Model):
    is_anonymized = models.BooleanField(default=False)
    billing_address = models.ForeignKey(BillingAdress, null=True, blank=True, on_delete=models.CASCADE)

    objects = models.Manager()
    anonymous_orders = AnonymousOrdersManager()

views.py:

def anonymous_address(request):
    orders = Order.anonymous_orders.all()
    
    context = {'orders': orders}
    return render(request, 'anonymous_address.html', context)
Niko
  • 3,012
  • 2
  • 8
  • 14
  • Thank you very much for your answer. With that solution I'd have to change every access to Order.billing_address to Order.anonymised_address in a big codebase. Maybe there would be a solution that better fits... – Frank Dec 16 '22 at 12:38
  • In that case, you could change the name of your model field for example `raw_billing_address`, Then, replace `anonymised_address` for `billing_address`. And, `billing_address` for `raw_billing_address` on the query. – Niko Dec 16 '22 at 14:57