DRY way to add created/modified by and time

Question:

Having something like

  • created_by
  • created_date
  • modified_by
  • modified_date

Would be a very common pattern for a lot of tables.

1) You can set created date automatically (but not others) in model.py with

created_date = models.DateTimeField(auto_now_add=True, editable=False)

2) You could do created/modified dates (but not by/user as don’t have request context) in model.py with

def save(self):
    if self.id:
        self.modified_date = datetime.now()
    else:
        self.created_date = datetime.now()
    super(MyModel,self).save()

3) You could set the created/modifed date and by in admin.py – but this doesn’t deal with non admin updates

def save_model(self, request, obj, form, change):
    if change:
        obj.modified_by = request.user
        obj.modified_date = datetime.now()
    else:
        obj.created_by = request.user
        obj.created_date = datetime.now()
    obj.save()

4) And the final place would be in the view.py which can do all 4, but doesn’t cover admin updates.

So realistically have to have logic spread out, at a minimum repeated in 3 & 4 (or a method on the model called from both, which will be missed)

Whats a better way? (I’ve been working with python/django for a couple of days so could easily be missing something obvious)

  • Can you do someting like @login_required e.g. @audit_changes
  • Can you get access to the request and current user in the model and centralise logic there?
Asked By: Ryan

||

Answers:

Can you import the User model object and call get_current()?

Also, I think you can call views in the admin.py.

Answered By: Timbadu

For timestamped models you probably want to look at django-model-utils or django-extensions. They each include abstract base classes which automatically handle of a created and last modified timestamp. You can either use these tools directly or look at how they solved the problem and come up with your own solution.

As for your other questions:

Can you do someting like @login_required e.g. @audit_changes

Potentially yes but you’d have to be very careful to keep things thread-safe. What you potentially could do is in your @audit_changes decorator, set a flag to enable auditing in a threadlocal. Then either in the save method of your models or in a signal handler, you could check for your audit flag and record your audit info if the flag had been set.

Can you get access to the request and current user in the model and centralise logic there?

Yes, but you’ll be making a tradeoff. As you’ve touched on a little bit, there is a very clear and intentional separation of concerns between Django’s ORM and it’s request/authentication handling bits. There are two ways ways to get information from the request (the current user) to the ORM (your model(s)). You can manually manage updating the creator/modifier information on your objects or you can set up a mechanism to automatically handle that maintenance work. If you take the manual approach (passing the information through method calls from the request in the view to the ORM), it will be more code to maintain/test but you keep the separation of concerns in place. With the manual approach, you will be in much better shape if you ever have to work with your objects outside of the request/response cycle (e.g. cron-scripts, delayed tasks, interactive shell). If you are ok with breaking down that separation of concerns, then you could setup something where you set a thread local with the current user in a middleware and then look at that thread local in the save method of your model. Converse to the manual approach, you’ll have less code to deal with but you’ll have a much harder time if you ever want to work with your objects outside of the request/response cycle. Additionally, you will have to be very careful to keep everything thread-safe with the more automated approach.

Answered By: SeanOC

The create/modification dates can be handled by Django now, so they can be implemented like:

class BaseModel(models.Model):
    created_date = models.DateTimeField(auto_now_add=True)
    modified_date = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

By adding this to a abstract model base class, it can be easily added to all models of the application.

Storing the user is harder, since the request.user is not available. As SeanOC mentioned, this is a separation of concerns between the web request, and model layer. Either you pass this field all the time, or store request.user in a threadlocal. Django CMS does this for their permission system.

class CurrentUserMiddleware(object):
    def process_request(self, request):
        set_current_user(getattr(request, 'user', None))

And the user tracking happens elsewhere:

from threading import local
_thread_locals = local()

def set_current_user(user):
    _thread_locals.user=user

def get_current_user():
    return getattr(_thread_locals, 'user', None)

For non-web environments (e.g. management commands), you’d have to call set_current_user at the start of the script.

Answered By: vdboor
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.