web-dev-qa-db-fra.com

Séparation de la logique métier et de l'accès aux données dans django

J'écris un projet dans Django et je constate que 80% du code est dans le fichier models.py. Ce code est déroutant et, après un certain temps, je ne comprends plus ce qui se passe réellement.

Voici ce qui me dérange:

  1. Je trouve moche que mon niveau de modèle (qui était supposé être responsable uniquement du travail avec les données d'une base de données) envoie également des courriels, passe de l'API à d'autres services, etc.
  2. De plus, je trouve inacceptable de placer la logique métier dans la vue, car cela devient difficile à contrôler. Par exemple, dans mon application, il existe au moins trois façons de créer de nouvelles instances de User, mais techniquement, il convient de les créer de manière uniforme.
  3. Je ne remarque pas toujours quand les méthodes et les propriétés de mes modèles deviennent non déterministes ni quand ils développent des effets secondaires.

Voici un exemple simple. Au début, le modèle User ressemblait à ceci:

class User(db.Models):

    def get_present_name(self):
        return self.name or 'Anonymous'

    def activate(self):
        self.status = 'activated'
        self.save()

Au fil du temps, cela s'est transformé en ceci:

class User(db.Models):

    def get_present_name(self): 
        # property became non-deterministic in terms of database
        # data is taken from another service by api
        return remote_api.request_user_name(self.uid) or 'Anonymous' 

    def activate(self):
        # method now has a side effect (send message to user)
        self.status = 'activated'
        self.save()
        send_mail('Your account is activated!', '…', [self.email])

Ce que je veux, c'est séparer les entités dans mon code:

  1. Entités de ma base de données, niveau base de données: que contient mon application?
  2. Entités de mon application, niveau de logique métier: que peut faire mon application?

Quelles sont les bonnes pratiques pour mettre en œuvre une telle approche pouvant être appliquée dans Django?

428
defuz

Il semble que vous vous interrogiez sur la différence entre le modèle de données et le modèle de domaine - ce dernier est l'endroit où vous pouvez trouver la logique métier et les entités telles que perçues par l'utilisateur final. , le premier est celui où vous stockez réellement vos données.

De plus, j'ai interprété la troisième partie de votre question comme suit: comment remarquer un échec pour garder ces modèles séparés.

Ce sont deux concepts très différents et il est toujours difficile de les séparer. Cependant, certains modèles et outils courants peuvent être utilisés à cette fin.

À propos du modèle de domaine

La première chose que vous devez reconnaître est que votre modèle de domaine ne concerne pas vraiment les données; il s'agit de actions et questions telles que "activer cet utilisateur", "désactiver cet utilisateur", "quels utilisateurs sont actuellement activés?" et "quel est le nom de cet utilisateur ? ". En termes classiques: il s'agit de requêtes et commandes.

Penser dans les commandes

Commençons par examiner les commandes de votre exemple: "activer cet utilisateur" et "désactiver cet utilisateur". La bonne chose à propos des commandes est qu’elles peuvent facilement être exprimées par de petits scénarios: quand-alors-alors:

étant donné un utilisateur inactif
lorsque l'administrateur active cet utilisateur
puis l'utilisateur devient actif
et un e-mail de confirmation est envoyé à l'utilisateur
et une entrée est ajoutée au journal système
(etc.)

De tels scénarios sont utiles pour voir comment une seule commande peut affecter différentes parties de votre infrastructure - dans ce cas, votre base de données (une sorte d'indicateur 'actif'), votre serveur de messagerie, votre journal système, etc.

De tels scénarios vous aident également à configurer un environnement de développement piloté par les tests.

Et enfin, penser aux commandes vous aide vraiment à créer une application orientée tâche. Vos utilisateurs apprécieront ceci :-)

Exprimer des commandes

Django fournit deux moyens simples d’exprimer des commandes; ce sont deux options valables et il n’est pas rare de mélanger les deux approches.

La couche service

Le module de service a déjà été décrit par @Hedde . Ici, vous définissez un module séparé et chaque commande est représentée sous forme de fonction.

services.py

def activate_user(user_id):
    user = User.objects.get(pk=user_id)

    # set active flag
    user.active = True
    user.save()

    # mail user
    send_mail(...)

    # etc etc

Utiliser des formulaires

L'autre façon consiste à utiliser un Django Form pour chaque commande. Je préfère cette approche, car elle combine plusieurs aspects étroitement liés:

  • exécution de la commande (que fait-il?)
  • validation des paramètres de commande (peut-il le faire?)
  • présentation de la commande (comment puis-je faire cela?)

forms.py

class ActivateUserForm(forms.Form):

    user_id = IntegerField(widget = UsernameSelectWidget, verbose_name="Select a user to activate")
    # the username select widget is not a standard Django widget, I just made it up

    def clean_user_id(self):
        user_id = self.cleaned_data['user_id']
        if User.objects.get(pk=user_id).active:
            raise ValidationError("This user cannot be activated")
        # you can also check authorizations etc. 
        return user_id

    def execute(self):
        """
        This is not a standard method in the forms API; it is intended to replace the 
        'extract-data-from-form-in-view-and-do-stuff' pattern by a more testable pattern. 
        """
        user_id = self.cleaned_data['user_id']

        user = User.objects.get(pk=user_id)

        # set active flag
        user.active = True
        user.save()

        # mail user
        send_mail(...)

        # etc etc

Penser aux requêtes

Votre exemple ne contenait aucune requête. Je me suis donc permis de créer quelques requêtes utiles. Je préfère utiliser le terme "question", mais requêtes est la terminologie classique. Les requêtes intéressantes sont: "Quel est le nom de cet utilisateur?", "Cet utilisateur peut-il se connecter?", "Afficher la liste des utilisateurs désactivés" et "Quelle est la répartition géographique des utilisateurs désactivés?"

Avant de répondre à ces questions, vous devez toujours vous poser deux questions: s'agit-il d'une requête présentation pour mes modèles uniquement et/ou d'une requête logique applicative liée à l'exécution de mon commandes, et/ou une requête reporting.

Les requêtes de présentation sont simplement faites pour améliorer l'interface utilisateur. Les réponses aux requêtes de la logique métier affectent directement l'exécution de vos commandes. Les requêtes de rapport servent uniquement à des fins d'analyse et ont des contraintes de temps plus souples. Ces catégories ne sont pas mutuellement exclusives.

L'autre question est: "est-ce que j'ai un contrôle complet sur les réponses?" Par exemple, lorsque vous interrogez le nom de l'utilisateur (dans ce contexte), nous n'avons aucun contrôle sur le résultat, car nous nous appuyons sur une API externe.

Faire des requêtes

La requête la plus élémentaire dans Django est l'utilisation de l'objet Manager:

User.objects.filter(active=True)

Bien entendu, cela ne fonctionne que si les données sont réellement représentées dans votre modèle de données. Ce n'est pas toujours le cas. Dans ces cas, vous pouvez envisager les options ci-dessous.

Tags et filtres personnalisés

La première alternative est utile pour les requêtes qui ne sont que des présentations: balises personnalisées et filtres de modèles.

template.html

<h1>Welcome, {{ user|friendly_name }}</h1>

template_tags.py

@register.filter
def friendly_name(user):
    return remote_api.get_cached_name(user.id)

Méthodes de requête

Si votre requête n’est pas simplement une présentation, vous pouvez ajouter des requêtes à votre services.py (si vous l’utilisez), ou introduire un module queries.py :

queries.py

def inactive_users():
    return User.objects.filter(active=False)


def users_called_publysher():
    for user in User.objects.all():
        if remote_api.get_cached_name(user.id) == "publysher":
            yield user 

Modèles de proxy

Les modèles de proxy sont très utiles dans le contexte de la logique métier et du reporting. Vous définissez en gros un sous-ensemble amélioré de votre modèle. Vous pouvez remplacer un QuerySet de base du gestionnaire en remplaçant la méthode Manager.get_queryset() .

models.py

class InactiveUserManager(models.Manager):
    def get_queryset(self):
        query_set = super(InactiveUserManager, self).get_queryset()
        return query_set.filter(active=False)

class InactiveUser(User):
    """
    >>> for user in InactiveUser.objects.all():
    …        assert user.active is False 
    """

    objects = InactiveUserManager()
    class Meta:
        proxy = True

Modèles de requête

Pour les requêtes intrinsèquement complexes, mais exécutées assez souvent, il existe une possibilité de modèles de requête. Un modèle de requête est une forme de dénormalisation dans laquelle les données pertinentes pour une requête unique sont stockées dans un modèle séparé. L'astuce consiste évidemment à maintenir le modèle dénormalisé synchronisé avec le modèle principal. Les modèles de requête ne peuvent être utilisés que si les modifications sont entièrement sous votre contrôle.

models.py

class InactiveUserDistribution(models.Model):
    country = CharField(max_length=200)
    inactive_user_count = IntegerField(default=0)

La première option consiste à mettre à jour ces modèles dans vos commandes. Ceci est très utile si ces modèles ne sont modifiés que par une ou deux commandes.

forms.py

class ActivateUserForm(forms.Form):
    # see above

    def execute(self):
        # see above
        query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
        query_model.inactive_user_count -= 1
        query_model.save()

Une meilleure option serait d'utiliser des signaux personnalisés. Ces signaux sont bien sûr émis par vos commandes. Les signaux présentent l'avantage de pouvoir synchroniser plusieurs modèles de requête avec votre modèle d'origine. En outre, le traitement du signal peut être déchargé sur des tâches en arrière-plan, à l'aide de Celery ou de cadres similaires.

signaux.py

user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])

forms.py

class ActivateUserForm(forms.Form):
    # see above

    def execute(self):
        # see above
        user_activated.send_robust(sender=self, user=user)

models.py

class InactiveUserDistribution(models.Model):
    # see above

@receiver(user_activated)
def on_user_activated(sender, **kwargs):
        user = kwargs['user']
        query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
        query_model.inactive_user_count -= 1
        query_model.save()

Le garder propre

Avec cette approche, il est ridiculement facile de déterminer si votre code reste propre. Suivez simplement ces instructions:

  • Mon modèle contient-il des méthodes qui font plus que gérer l'état de la base de données? Vous devriez extraire une commande.
  • Mon modèle contient-il des propriétés qui ne correspondent pas aux champs de la base de données? Vous devriez extraire une requête.
  • Mon modèle référence-t-il une infrastructure autre que ma base de données (telle que la messagerie)? Vous devriez extraire une commande.

Il en va de même pour les vues (car les vues souffrent souvent du même problème).

  • Mon vue gère-t-elle activement les modèles de base de données? Vous devriez extraire une commande.

Quelques références

documentation Django: modèles de proxy

documentation Django: signaux

Architecture: Domain Driven Design

563
publysher

J'implémente généralement une couche de service entre les vues et les modèles. Cela ressemble à l'API de votre projet et vous donne une bonne vue en hélicoptère de ce qui se passe. J'ai hérité de cette pratique d'un collègue à moi qui utilise beaucoup cette technique de superposition avec des projets Java (JSF), par exemple:

models.py

class Book:
   author = models.ForeignKey(User)
   title = models.CharField(max_length=125)

   class Meta:
       app_label = "library"

services.py

from library.models import Book

def get_books(limit=None, **filters):
    """ simple service function for retrieving books can be widely extended """
    if limit:
        return Book.objects.filter(**filters)[:limit]
    return Book.objects.filter(**filters)

views.py

from library.services import get_books

class BookListView(ListView):
    """ simple view, e.g. implement a _build and _apply filters function """
    queryset = get_books()

Remarquez que je prends généralement les modèles, les vues et les services au niveau du module et les sépare encore plus en fonction de la taille du projet.

131
Hedde van der Heide

Tout d'abord, ne vous répétez pas .

Ensuite, veillez à ne pas trop d'ingénieurs, c'est parfois une perte de temps qui fait perdre la concentration sur ce qui est important. Passez en revue le zen of python de temps en temps.

Regardez les projets actifs

  • plus de personnes = plus besoin d'organiser correctement
  • les référentiel Django ils ont une structure simple.
  • le référentiel pip ils ont une structure de répertoires straigtforward.
  • le référentiel de structure est également un bon exemple à regarder.

    • vous pouvez placer tous vos modèles sous _yourapp/models/logicalgroup.py_
  • par exemple, User, Group et les modèles associés peuvent aller sous _yourapp/models/users.py_
  • par exemple, Poll, Question, Answer ... peut aller sous _yourapp/models/polls.py_
  • chargez ce dont vous avez besoin dans ___all___ à l'intérieur de _yourapp/models/__init__.py_

Plus d'informations sur MVC

  • le modèle correspond à vos données
    • cela inclut vos données réelles
    • cela inclut également vos données de session/cookie/cache/fs/index
  • l'utilisateur interagit avec le contrôleur pour manipuler le modèle
    • cela peut être une API ou une vue qui enregistre/met à jour vos données
    • ceci peut être réglé avec _request.GET_/_request.POST_ ... etc
    • pensez également à la pagination ou au filtrage .
  • les données mettent à jour la vue
    • les modèles prennent les données et les mettent en forme en conséquence
    • Les API, même sans modèles, font partie de la vue; par exemple. tastypie ou piston
    • cela devrait également représenter le middleware.

Profitez de middleware / tags du je

  • Si vous avez besoin de travail pour chaque demande, le middleware est une solution.
    • par exemple. ajout d'horodatages
    • par exemple. mise à jour des métriques sur les visites de page
    • par exemple. remplir une cache
  • Si vous avez des fragments de code qui se répètent toujours pour la mise en forme des objets, les balises d'identification de modèle sont bonnes.
    • par exemple. onglet actif/chapelure

Tirez parti de gestionnaires de modèles

  • créer User peut aller dans une UserManager(models.Manager).
  • les détails détaillés pour les instances doivent aller sur le _models.Model_.
  • les détails gory de queryset pourraient aller dans un _models.Manager_.
  • vous voudrez peut-être créer un User un à la fois, de sorte que vous pensiez qu'il devrait vivre sur le modèle lui-même, mais lors de la création de l'objet, vous n'avez probablement pas tous les détails:

Exemple:

_class UserManager(models.Manager):
   def create_user(self, username, ...):
      # plain create
   def create_superuser(self, username, ...):
      # may set is_superuser field.
   def activate(self, username):
      # may use save() and send_mail()
   def activate_in_bulk(self, queryset):
      # may use queryset.update() instead of save()
      # may use send_mass_mail() instead of send_mail()
_

Utilisez des formulaires autant que possible

Une grande partie du code standard peut être éliminée si vous avez des formulaires mappés à un modèle. Le ModelForm documentation est très bon. Séparer le code des formulaires du code du modèle peut être utile si vous avez beaucoup de personnalisation (ou évitez parfois les erreurs d'importation cycliques pour des utilisations plus avancées).

Utilisez commandes de gestion si possible

  • par exemple. _yourapp/management/commands/createsuperuser.py_
  • par exemple. _yourapp/management/commands/activateinbulk.py_

si vous avez une logique métier, vous pouvez la séparer

  • _Django.contrib.auth_ tilise backends , tout comme db a un backend ... etc.
  • ajoutez un setting pour votre logique métier (par exemple, _AUTHENTICATION_BACKENDS_)
  • vous pouvez utiliser _Django.contrib.auth.backends.RemoteUserBackend_
  • vous pouvez utiliser _yourapp.backends.remote_api.RemoteUserBackend_
  • vous pouvez utiliser _yourapp.backends.memcached.RemoteUserBackend_
  • déléguer la logique métier difficile au backend
  • assurez-vous de définir les attentes directement sur l'entrée/la sortie.
  • changer la logique métier est aussi simple que de changer un paramètre :)

exemple de backend:

_class User(db.Models):
    def get_present_name(self): 
        # property became not deterministic in terms of database
        # data is taken from another service by api
        return remote_api.request_user_name(self.uid) or 'Anonymous' 
_

pourrait devenir:

_class User(db.Models):
   def get_present_name(self):
      for backend in get_backends():
         try:
            return backend.get_present_name(self)
         except: # make pylint happy.
            pass
      return None
_

plus sur les modèles de conception

plus sur les limites de l'interface

  • Le code que vous souhaitez utiliser fait-il vraiment partie des modèles? -> _yourapp.models_
  • Le code fait-il partie de la logique métier? -> _yourapp.vendor_
  • Le code fait-il partie des outils/bibliothèques génériques? -> _yourapp.libs_
  • Le code fait-il partie des bibliothèques de la logique applicative? -> _yourapp.libs.vendor_ ou _yourapp.vendor.libs_
  • En voici un bon exemple: pouvez-vous tester votre code indépendamment?
    • oui bien :)
    • non, vous pouvez avoir un problème d'interface
    • quand il y a séparation nette, unittest devrait être un jeu d'enfant avec l'utilisation de moquerie
  • La séparation est-elle logique?
    • oui bien :)
    • non, vous pouvez avoir du mal à tester ces concepts logiques séparément.
  • Pensez-vous que vous aurez besoin de refactoriser 10 fois plus de code?
    • oui, non bon, non bueno, refactor pourrait être beaucoup de travail
    • non, c'est juste génial!

En bref, vous pourriez avoir

  • _yourapp/core/backends.py_
  • _yourapp/core/models/__init__.py_
  • _yourapp/core/models/users.py_
  • _yourapp/core/models/questions.py_
  • _yourapp/core/backends.py_
  • _yourapp/core/forms.py_
  • _yourapp/core/handlers.py_
  • _yourapp/core/management/commands/__init__.py_
  • _yourapp/core/management/commands/closepolls.py_
  • _yourapp/core/management/commands/removeduplicates.py_
  • _yourapp/core/middleware.py_
  • _yourapp/core/signals.py_
  • _yourapp/core/templatetags/__init__.py_
  • _yourapp/core/templatetags/polls_extras.py_
  • _yourapp/core/views/__init__.py_
  • _yourapp/core/views/users.py_
  • _yourapp/core/views/questions.py_
  • _yourapp/core/signals.py_
  • _yourapp/lib/utils.py_
  • _yourapp/lib/textanalysis.py_
  • _yourapp/lib/ratings.py_
  • _yourapp/vendor/backends.py_
  • _yourapp/vendor/morebusinesslogic.py_
  • _yourapp/vendor/handlers.py_
  • _yourapp/vendor/middleware.py_
  • _yourapp/vendor/signals.py_
  • _yourapp/tests/test_polls.py_
  • _yourapp/tests/test_questions.py_
  • _yourapp/tests/test_duplicates.py_
  • _yourapp/tests/test_ratings.py_

ou quelque chose d'autre qui vous aide; trouver les interfaces dont vous avez besoin et les limites vous aideront.

65
dnozay

Django utilise un type de MVC légèrement modifié. Il n'y a pas de concept de "contrôleur" dans Django. Le proxy le plus proche est une "vue", ce qui tend à semer la confusion avec les conversions MVC car, dans MVC, une vue ressemble davantage au "modèle" de Django.

Dans Django, un "modèle" n'est pas simplement une abstraction de base de données. À certains égards, il partage le devoir avec la "vue" de Django en tant que contrôleur de MVC. Il contient l'intégralité du comportement associé à une instance. Si cette instance doit interagir avec une API externe dans le cadre de son comportement, il s'agit toujours d'un code modèle. En fait, les modèles ne sont pas obligés d'interagir avec la base de données. Vous pouvez donc concevoir des modèles qui existent entièrement en tant que couche interactive vers une API externe. C'est un concept beaucoup plus libre de "modèle".

24
Chris Pratt

Dans Django, la structure MVC est, comme Chris Pratt l'a dit, différente du modèle MVC classique utilisé dans d'autres frameworks. Je pense que la raison principale est d'éviter une structure d'application trop stricte, comme cela se produit dans d'autres frameworks MVC tels que CakePHP.

Dans Django, MVC a été implémenté de la manière suivante:

La couche de vue est divisée en deux. Les vues ne doivent être utilisées que pour gérer les requêtes HTTP, elles sont appelées et y répondent. Les vues communiquent avec le reste de votre application (formulaires, modèles, classes personnalisées ou, dans des cas simples, directement avec des modèles). Pour créer l'interface, nous utilisons des modèles. Les modèles ressemblent à des chaînes pour Django, cela mappe un contexte, et ce contexte a été communiqué à la vue par l'application (lorsque la vue le demande).

La couche modèle fournit l'encapsulation, l'abstraction, la validation, l'intelligence et rend vos données orientées objet (on dit qu'un jour, le SGBD le sera également). Cela ne signifie pas que vous devez créer d’énormes fichiers models.py (en fait, un très bon conseil est de diviser vos modèles en différents fichiers, de les placer dans un dossier appelé 'modèles', de créer un fichier '__init__.py' dans ce dossier. dossier dans lequel vous importez tous vos modèles et utilisez enfin l'attribut 'app_label' de models.Model class). Le modèle devrait vous éviter d'utiliser des données, cela simplifiera votre application. Vous devez également, si nécessaire, créer des classes externes, telles que des "outils" pour vos modèles. Vous pouvez également utiliser l'héritage dans les modèles, en définissant l'attribut "abstrait" de la classe Meta de votre modèle sur "True".

Où est le reste? Eh bien, les petites applications Web sont généralement une sorte d’interface avec les données. Dans certains petits programmes, il serait suffisant d’utiliser des vues pour interroger ou insérer des données. Les cas les plus courants utiliseront Forms ou ModelForms, qui sont en réalité des "contrôleurs". Ce n'est pas autre qu'une solution pratique à un problème commun, et très rapide. C'est ce qu'un site Web fait.

Si les formulaires ne vous conviennent pas, vous devez créer vos propres classes pour faire de la magie. Un très bon exemple de ceci est l’application admin: vous pouvez lire le code ModelAmin, cela fonctionne en tant que contrôleur. Il n'y a pas de structure standard, je vous suggère donc d'examiner les applications Django existantes, cela dépend de chaque cas. C’est ce que les développeurs Django voulaient, vous pouvez ajouter une classe d’analyseur XML, une classe de connecteur d’API, ajouter Celery pour la réalisation de tâches, torsadé pour une application basée sur un réacteur, utiliser uniquement l’ORM, créer un service Web, modifier l’application admin et bien plus encore… C’est votre responsabilité de créer un code de bonne qualité, de respecter la philosophie de MVC ou non, d’en faire un module et de créer vos propres couches d’abstraction. C'est très flexible.

Mon conseil: lisez autant de code que vous pouvez, il y a beaucoup d'applications Django, mais ne les prenez pas si au sérieux. Chaque cas est différent, les schémas et la théorie aident, mais pas toujours, c’est une science imprécise, Django vous fournit simplement de bons outils que vous pouvez utiliser pour atténuer certaines difficultés (telles que l'interface d'administration, la validation de formulaire Web, i18n). , mise en œuvre de modèles d’observateur, toutes les précédentes et d’autres), mais les bonnes conceptions viennent de designers expérimentés.

PS .: utilisez la classe 'User' de l’authentification (de Django standard), vous pouvez par exemple créer des profils d’utilisateur, ou au moins lire son code, cela vous sera utile.

6
Nate Gentile

Je suis principalement d'accord avec la réponse choisie ( https://stackoverflow.com/a/12857584/871392 ), mais je souhaite ajouter une option dans la section Création de requêtes.

On peut définir des classes QuerySet pour les modèles pour les requêtes de filtre et les fils. Après cela, vous pouvez créer un proxy pour cette classe de requête pour le gestionnaire de modèle, comme le font les classes de gestionnaire et QuerySet intégrées.

Bien que, si vous deviez interroger plusieurs modèles de données pour obtenir un modèle de domaine, il me semble plus raisonnable de placer cela dans un module séparé, comme suggéré précédemment.

0
l0ki

Je serais d'accord avec vous. Il y a beaucoup de possibilités dans Django, mais le meilleur endroit pour commencer est de passer en revue philosophie de conception de Django .

  1. L'appel d'une API à partir d'une propriété de modèle ne serait pas idéal. Il semble plus logique de faire quelque chose comme cela dans la vue et éventuellement de créer une couche de service pour que tout reste au sec. Si l'appel à l'API n'est pas bloquant et que l'appel est coûteux, il peut être judicieux d'envoyer la demande à un agent de service (un agent qui consomme depuis une file d'attente).

  2. Selon la philosophie de conception de Django, les modèles englobent tous les aspects d'un "objet". Ainsi, toute logique métier liée à cet objet doit y vivre:

Inclut toute la logique de domaine pertinente

Les modèles doivent englober tous les aspects d’un "objet", conformément au modèle de conception Active Record de Martin Fowler.

  1. Les effets secondaires que vous décrivez sont évidents, la logique ici pourrait être mieux décomposée en groupes de requêtes et en gestionnaires. Voici un exemple:

    models.py

    import datetime
    
    from djongo import models
    from Django.db.models.query import QuerySet
    from Django.contrib import admin
    from Django.db import transaction
    
    
    class MyUser(models.Model):
    
        present_name = models.TextField(null=False, blank=True)
        status = models.TextField(null=False, blank=True)
        last_active = models.DateTimeField(auto_now=True, editable=False)
    
        # As mentioned you could put this in a template tag to pull it
        # from cache there. Depending on how it is used, it could be
        # retrieved from within the admin view or from a custom view
        # if that is the only place you will use it.
        #def get_present_name(self):
        #    # property became non-deterministic in terms of database
        #    # data is taken from another service by api
        #    return remote_api.request_user_name(self.uid) or 'Anonymous'
    
        # Moved to admin as an action
        # def activate(self):
        #     # method now has a side effect (send message to user)
        #     self.status = 'activated'
        #     self.save()
        #     # send email via email service
        #     #send_mail('Your account is activated!', '…', [self.email])
    
        class Meta:
            ordering = ['-id']  # Needed for DRF pagination
    
        def __unicode__(self):
            return '{}'.format(self.pk)
    
    
    class MyUserRegistrationQuerySet(QuerySet):
    
        def for_inactive_users(self):
            new_date = datetime.datetime.now() - datetime.timedelta(days=3*365)  # 3 Years ago
            return self.filter(last_active__lte=new_date.year)
    
        def by_user_id(self, user_ids):
            return self.filter(id__in=user_ids)
    
    
    class MyUserRegistrationManager(models.Manager):
    
        def get_query_set(self):
            return MyUserRegistrationQuerySet(self.model, using=self._db)
    
        def with_no_activity(self):
            return self.get_query_set().for_inactive_users()
    

    admin.py

    # Then in model admin
    
    class MyUserRegistrationAdmin(admin.ModelAdmin):
        actions = (
            'send_welcome_emails',
        )
    
        def send_activate_emails(self, request, queryset):
            rows_affected = 0
            for obj in queryset:
                with transaction.commit_on_success():
                    # send_email('welcome_email', request, obj) # send email via email service
                    obj.status = 'activated'
                    obj.save()
                    rows_affected += 1
    
            self.message_user(request, 'sent %d' % rows_affected)
    
    admin.site.register(MyUser, MyUserRegistrationAdmin)
    
0
radtek