2

J'essaye d'écrire une fonction PostgreSQL personnalisée dans Django qui va coercer les dates-heures dans un fuseau horaire spécifié à l'intérieur d'un ensemble de requêtes. Ma première passe à la fonction db ressemble à ceci:DjangoORM: Résolution de l'expression F dans la fonction de base de données personnalisée

from django.db.models.expressions import Func 

class DateTimeInTimezone(Func): 
    template="%(expressions)s AT TIME ZONE %(tz_info)s" 

Cette fonction fonctionne dans le cas simple où je passe une chaîne de fuseau horaire dans la fonction directement comme ceci:

MyModel.objects.filter(timefield__gte=DateTimeInTimezone(Now(), tz_info='EST')) 

Cependant, il ne travailler dans le cas le plus complexe, où le fuseau horaire est défini sur un champ du modèle. Prenons l'exemple suivant artificiel:

class User(models.Model): 
    time_zone = models.CharField() 

class Meeting(models.Model): 
    users = models.ManyToManyField(User, related_name='meetings') 
    start_time = models.DateTimeField() # in UTC 
    end_time = models.DateTimeField() # in UTC 

Pour répondre à la question « Qu'est-ce que les utilisateurs seront à une réunion à 12 h aujourd'hui, heure locale? », Je aurais besoin une certaine variation de cette queryset:

noon_utc = ... 
User.objects.filter(
    meetings__start_time__lte=DateTimeInTimezone(noon_utc, tz_info=F('time_zone')), 
    meetings__end_time__gt=DateTimeInTimezone(noon_utc, tz_info=F('time_zone')) 
) 

Comme actuellement écrit, cependant, DateTimeInTimezone injectera simplement la chaîne F('time_zone') dans le sql plutôt que de résoudre le champ.

Est-il possible d'ajouter le support pour F Expressions à cette fonction? Y a-t-il une autre approche que je devrais envisager?

Répondre

1

Une solution simple est possible avec paramètre arg_joiner:

class DateTimeInTimezone(Func): 
    function = '' 
    arg_joiner = ' AT TIME ZONE ' 

    def __init__(self, timestamp, tz_info): 
     super(DateTimeInTimezone, self).__init__(timestamp, tz_info) 

La méthode __init__ est utilisée uniquement dans le but de lisibilité avec des noms clairs des paramètres. Alors arity n'est pas important si les paramètres sont déclarés par __init__.

A oneliner fonction est utile pour le développement rapide si la lisibilité n'est pas important:

...filter(
    meetings__start_time__lte=Func(noon_utc, tz_info=F('time_zone'), arg_joiner=' AT TIME ZONE ', function=''), 
) 

Vérifié:

>>> qs = User.objects.filter(...) 
>>> print(str(qs.query)) 
SELECT ... WHERE ("app_meeting"."start_time" <= ((2017-10-03 08:18:12.663640 AT TIME ZONE "app_user"."time_zone")) AND ...) 
+0

Hé, c'est génial! Merci d'avoir suggéré cette solution. J'ai fait de votre réponse la réponse acceptée car il est définitivement préférable de corriger les singes (comme vous l'avez mentionné dans votre commentaire). – chukkwagon

1

Trouvé une solution acceptable. J'ai outrepassé la méthode as_sql pour une fonction comme celle-ci, permettant aux django internes de résoudre l'expression F puis de la séparer dans un kwarg que je pourrais utiliser dans une partie différente du modèle.

class DateTimeInTimezone(Func): 
''' 
Coerce a datetime into a specified timezone 
''' 
template="%(expressions)s AT TIME ZONE %(tz_info)s" 
arity = 2 

def as_sql(self, compiler, connection, function=None, template=None, arg_joiner=None, **extra_context): 
    connection.ops.check_expression_support(self) 
    sql_parts = [] 
    params = [] 
    for arg in self.source_expressions: 
     arg_sql, arg_params = compiler.compile(arg) 
     sql_parts.append(arg_sql) 
     params.extend(arg_params) 
    data = self.extra.copy() 
    data.update(**extra_context) 
    # Use the first supplied value in this order: the parameter to this 
    # method, a value supplied in __init__()'s **extra (the value in 
    # `data`), or the value defined on the class. 
    if function is not None: 
     data['function'] = function 
    else: 
     data.setdefault('function', self.function) 
    template = template or data.get('template', self.template) 
    arg_joiner = arg_joiner or data.get('arg_joiner', self.arg_joiner) 
    data['expressions'] = data['field'] = arg_joiner.join(sql_parts) 
    parts = data['expressions'].split(', ') 
    data['expressions'] = parts[0] 
    data['tz_info'] = parts[1] 
    return template % data, params 

J'ai ajouté les trois lignes entre l'attribution de data['expressions'] et le return template % data, params final. Ce n'est pas une bonne solution à long terme car les internes de django pour cette méthode pourraient changer dans la prochaine version, mais cela convient à mes besoins pour le moment.

+0

+1 cela fonctionne, mais patching singe est le dernier recours, car la maintenance pourrait être laborieuse après la mise à niveau de Django. – hynekcer