Search code examples
djangourlnonetype

Django - How to cast "None" from url kwargs to None without repeating yourself


I have a model like this:

#models.py
class Location(BaseArticle):
    name = models.CharField(max_length=200)
    parent_location = models.ForeignKey("self",
                                        blank=True,
                                        null=True,
                                        help_text="Fill in if this location is a smaller part of another location.",
                                        on_delete=models.SET_NULL)
    description = HTMLField(blank=True,
                            null=True,
                            help_text="A description of the location and important features about it")

    class Meta:
        unique_together = ('name', 'parent_location')

I decided in my urls to uniquely identify each Location through its own name, and the name of its parent-element.

# urls.py
urlpatterns = [
    path('locations/<str:parent_location_name>/<str:location_name>', wiki_views.LocationView.as_view(), name='location'),
]

# example url to location "Seedy Bar" located in "City"
# https://<DOMAIN>/locations/city/seedy bar
# example url to location "City" that does not have a parent_location
# https://<DOMAIN>/locations/none/city

As you can see from the examples, Locations where parent_location is null/None are possible. I decided to convert None to "None" for these urls, though I'm not set on what None gets transformed to. Now as you may realize this means that whenever I do anything with Location, I need to pay attention to convert "None" to None in the views as necessary, which gets annoying really fast.

To follow DRY I was wondering what the best way would be to deal with this, or have I designed myself into a corner here?

Edit: I believe the best solution for this might be within the Location model or its Manager. Fixing it there means this logic is handled for all views and queries, whereas a solution for this problem on the view-level would mean that the solution needs to be implemented for every view that might make a query to Location with `"None".

What I came up with so far:

Given that Managers are the interface through which you interact with models I checked whether they have anything and stumbled over modifying get_queryset. As I do not call get_queryset myself however, I'm not sure if functions like filter/exclude any others I might not even know about actually call it. However, I generally like this approach best as it keeps the logic on how to handle this stuff with the model/manager, where it belongs in my eyes.

Another alternative would of course be to write my own Mixin to fix this issue on the view-level, but that means I still need to remember every time that this is an issue.


Solution

  • Thanks to the advice of @Aayush Agrawal I just barely escaped from committing to a bad practice in this regard. In hindsight I agree, it is not a good idea to use a Manager for this, as while that might work, it would also obfuscate inputs and produce unexpected results in the future.

    Thus I opted to go for the Mixin solution and Implemented the following:

    # settings.py
    NONE_STRING = 'None'
    
    
    
    # views.py
    from django.conf import settings
    
    class ReplaceNoneMixin(object):
        """Mixin to deal with URL Parameters that represent None. This mixin takes the name of a url parameter and assumes
        that if it represents None that it has the value of settings.NONE_STRING. If the parameter is encountered with that
        value, the value is changed from NONE_STRING to None."""
        kwargs: dict
        noneable_parameters_keys: list
    
        def setup(self, request, *args, **kwargs):
            """Changes the value of all url parameters specified in noneable_parameters_keys list. If the parameter has the
             settings.NONE_STRING value in kwargs then the value is set to None."""
            super().setup(request, *args, **kwargs)
            if isinstance(self.noneable_parameters_keys, str):
                self.noneable_parameters_keys = [self.noneable_parameters_keys]
    
            for parameter in self.noneable_parameters_keys:
                if parameter in self.kwargs and self.kwargs[parameter] == settings.NONE_STRING:
                    self.kwargs[parameter] = None