Model update on GET view render

I am not sure if my design is here is sound, seeking feedback on making better choices.

Basically, users are members of multiple teams (m2m through ‘membership’). All of my views render a navbar with content below. the navbar shows the name of the currently selected team with the relevant content in the main section below the navbar. Clicking on the name of the team in the nav bar drops down a list of other teams available, and when clicked the name in the nav bar updates to the new selection and the content in the main frame updates to reflect the details of the newly selected team. My user model has a “last_team” property which I would like to update any time a new team is selected OR whenever a user logs out. I believe that a logout signal is probably my best bet for updating that property when a user clicks the logout button. However, I expect most people will just close the tab or the browser and move on with their lives. As such, I want to update the last_team property of the currently logged in user any time they switch teams. I was thinking it would make sense to set that in the view itself, but I have two serious reservations. 1) I’m concerned about adding a model write to every get request for almost every page. 2) Updating a model field when rendering a GET feels like reliance on a side effect of calling a view, which doesn’t feel like it meets the “explicit is better” django philosophy.

My first thought on a revision here is to only update the user’s “last_team” property when it is actually changed, which would only do the db write many fewer times than if it were set every time the view is called. However, this doesn’t address the concern that I’m relying on an implicit side effect to update the model on a GET view.

Model Code:

user.CustomUser

...

class CustomUser(AbstractUser):
    avatar = models.FileField(upload_to=_get_avatar_filename, blank=True, validators=[validate_profile_picture])
    language = models.CharField(max_length=10, blank=True, null=True)
    timezone = models.CharField(max_length=100, blank=True, default="")
    last_team = models.OneToOneField('teams.Team', related_name='last_team', blank=True, null=True, default=None, on_delete=models.CASCADE)
    default_team = models.OneToOneField('teams.Team', related_name='default_team', blank=True, null=True, default=None, on_delete=models.CASCADE)

  ...

team.Team:

...
class Team(BaseModel):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)
    members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="teams", through="Membership")
    is_public = models.BooleanField(default=False)
    public_desc = models.TextField(max_length=400, default='', null=True, blank=True)
...

team.Membership

...
class Membership(BaseModel):
    team = models.ForeignKey(Team, on_delete=models.CASCADE)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    role = models.CharField(max_length=100, choices=roles.ROLE_CHOICES)
...
    class Meta:
        # Ensure a user can only be associated with a team once.
        unique_together = ("team", "user")
...

View code:

@login_and_team_required
def team_home(request, team_slug):
    assert request.team.slug == team_slug
    team_profile = TeamProfile.objects.get(pk=request.user.pk)
    # This is where I'd check/update user.last_team, I think?
    # Something like:
    # if user.last_team.slug != team_slug: 
    #   user.last_team=request.team
    #   user.save()
    return render(
        request,
        "web/app_home.html",
        context={
            "team": request.team,
            "active_tab": "dashboard",
            "page_title": _("{team} Dashboard").format(team=request.team),
            "team_profile": team_profile,
        },
    )

I’m curious, under what circumstances is this last_team property going to change if it’s not selected? In other words, what situations is the “whenever a user logs out” going to cover that “a new team is selected” will not?

General idea: I’d probably change the “GET” on the “new team is selected” to being a POST to make it explicit that an attribute is being changed. What the post returns can still be the new page.

Also, I’m curious about this in your CustomUser model:

Only one person can have a team as their last_team? If Team X is my last_team, then you can’t select Team X? Seems odd to me.

I think the answer is none, meaning that I only have one use case to solve for, which is “any time the team is changed.” Excellent point, thank you.

Sigh, my noobness is showing. I was kind of naively thinking “GET is for displaying stuff, POST is for changing stuff. I’m not rendering or processing a form, so this should be a GET.” I wasn’t fully thinking through the fact GET and POST are both requests for which I can render a response. Doing the model update in a POST clearly addresses my concerns about being explicit.

Good catch, thanks. I’ve only just written that code and haven’t fully exercised it yet. This is certainly not what I actually want. Pretty sure I just need to change it from a 1:1 to an FK, as the intent was for each user to have only one team as their last_team, but that team could be the last_team team for any number of users.