Using arrayfield to match request.path for active link

I’m looking to create a viewmixin to help identify which of my navigation links are currently active based on the request.path.

I’ve typically got away with some basic regex and/or startswith string matching for this sort of thing. But I’ve now got a pretty long list of matches and finding it difficult to match rules to the request url without flagging wrong/multiple links.

Instead, I’m thinking about creating an arrayfield in the model which stores all my navlinks. And then in that field, storing lists of the namespaces and url_name which you can get from request.resolver_match. Then, I’d simply lookup the table on each view using the resolver against the arrayfield, and flag the correct link as active using context to the view.

Any thoughts on this solution? I’ve not used arrayfields before, but seems like this might work.

Just for addl. context. One of the things I’m struggling with, is flagging active nav links which have the correct root of the request url, but don’t exactly match the url.

Ior example, if request.path is /portal/profile/home/, I might want to match the /portal/profile/ nav link, but not the /portal/ nav link. Hence, why some added flexibility on filtering namespaces might help.

I’m not sure I understand what the underlying objective is here, but I’m also not sure that it’s necessary for me to do so.

What I’m guessing the situation is that you’re trying to limit some number of links on your page based on the current URL. Is this correct?

From what I can tell, certainly an ArrayField appears like it would work for what you’re trying to do. Your other option is to create a ManyToMany relationship on your navlinks model with itself, where that model enumerates all the related entries to which an entry can link.

Either one can be made to work. The “DB purist” in me definitely would prefer the M2M solution.

I already have a fk in the nav model referencing ‘self’, and using that to add some structure to navlinks. For example, being able to set parents and childs.

But what I’m after here, is a way to change the style (e.g. bold) the navlink which is currently active based on the viewed page. And where things get difficult is due to having pages which have the same root url as a navlink, but don’t exactly match the navlink url. For example, you can navigate to a navlink homepage, and then within that page, you run deeper into the url tree. But at the same time, you want to keep that same navlink homepage style bolded as you run deeper.

One way would be to use string matching via startswith. So, you can just match any urls which relate to that navlink by matching its root. But – I have multiple navlinks with the same root url. So, you’d be changing the style on multiple navlinks.

At the moment, I’m doing it like this. Which feels messy and wrong:

# dict of possible root url patterns seen by request.path (key) matched to corrent navlink url targets (items).
nav_active_target_patterns = {
    r'^/page/page3/': '/page/page3/',
    r'^/page/page2/page4/': '/page/page2/page4/',
}

self.sidenav_active_target = None
for link in nav_link_qs:

    if link.target == request.path: # first look for an exact match over all possible navlinks
        self.sidenav_active_target = link.target
        break

  
for pattern, nav_active_target in self.nav_active_target_patterns.items():

    if re.compile(pattern).match(request.path): # if no exact match, use pattern matching
        self.sidenav_active_target = nav_active_target
        break

So, for example, say I had /page/ as a possible navlink target within nav_link_qs, and the current request path was /page/. I’d be able to get that exact match and flag that navlink as currently active. Likewise for /page/page3/ and page/page2/page4/.

But say instead the user navigates to page/page3/, and then within that page, navigates to page/page3/subpage1/. I’d want to keep page/page3/ navlink active, but no longer have an exact match, as I don’t have a navlink for page/page3/subpage1/.

Just curious. Isnt there a django-nav-links package or something like it?

Are these links being rendered as part of a full page refresh, or are you working with some type of SPA-like structure where you’re trying to update the existing links on the page?

Yeh, full page refresh each with their own view. I’m using the above code in a viewmixin for each.

Since I don’t know your application’s architecture, I don’t know how much of this might apply.

We have a dynamic menu structure, where each person’s menu may change based upon their current role and the time of day/day of week. (Our menu model contains a set of predicates that define when an individual menu entry is to be rendered.)

If I were needing to do this, I would create a many-to-many relationship between entries in my model containing my url references. Then, in prepping the menu for rendering, I would check each entry to see if it’s related to the current entry, and add the appropriate CSS class name to the entry depending upon the results of that test.

mmm, ok. I’ll need to read up on that. I guess by predicate, you mean like Django’s annotate + Q function? But it is just testing the model instance against some set of rules before actually calling and modifying the queryset?

And do note that not every url is in my navlink model. So, I guess for this to work, I need to create a model to house all urls which need to relate back to an active link class, and then build a whole set of manytomany’s in order to map their relationships.

Basically. More like a dict that can be used as the kwargs of a filter or annotation.

Not in this specific case. It’s a situation where the query uses that filter to only retrieve a subset of the complete menu.

I’ll refer to my previous comment - this is what works for us. I don’t know the architecture of your system to identify how much of it may apply to you, or what changes you may need to make to this concept for it to be useful to you.

But there wouldn’t need to be an “active link” class. Under the assumption that all urls would need to be in a navlink model to be able to identify the related active links, you could create this as a single many-to-many relationship between navlink instances.

Note, URLs that neither have any active links nor are active links to any other URL wouldn’t need to be in that model. In our case, that model carries a bunch of information that defines under what circumstances a menu entry is to be included in the menu, so every url that is to be directly accessible is in the model, but subordinate urls for things like AJAX interactions, are not.

1 Like

While on the topic. I’m curious the best way to set values on querysets containing related fields which reference the same model.

For example, if I have the below two fields in a model –

    parent = models.ForeignKey(
        'self',
        on_delete=models.CASCADE,
        related_name='child'
    )

    field = models.CharField(max_length=255)

And I pull a queryset for the above model in a view – I can iterate through that queryset and change values of the ‘field’ attribute directly. e.g. model.field = ‘value’. I can then represent that modified queryset in a template without actually saving the modified queryset to the db. Likewise, I could also follow a similar strategy to add a new custom attribute to every queryset before sending to the template.

However, I’m not sure how to do this for the self referenced querysets in the above example. Doing a loop of any ‘child’ querysets and trying to assign the ‘field’ attribute of those querysets does not work as I’d expect. e.g. for child in qs: child.field = ‘value’, won’t actually assign the value for related querysets so they can be viewed in the template.

So, I guess this is where annotate + Q and/or predicates may help, as you mentioned. Curious if you can provide an example of the latter, or even point out if I’m doing something wrong in the above.

If I’m following you correctly, qs is some queryset on YourModel.

Therefore, if you are iterating over qs as in for a_model in qs, then a_model is an instance of YourModel. Therefore, it has an automatically created related object manager named child. That’s the key point here - child is the related manager, not an instance of child. If you want to do some operation on the individual instances of child, you either need to iterate over a_model.child.all(), or if appropriate, make some change to those instances (e.g. a_model.child.update(a_field="some value")), or you can annotate values on those instances (e.g. a_model.child.annotate(annot="some value")).

1 Like