Django-rest-framework: Le sérialiseur DateTimeField contient des informations de fuseau horaire inattendues

Créé le 13 déc. 2015  ·  34Commentaires  ·  Source: encode/django-rest-framework

J'ai TIME_ZONE = 'Asia/Kolkata' et USE_TZ = True dans mes paramètres.

Lorsque je crée un nouvel objet avec l'API explorable, le sérialiseur affiche l'objet nouvellement créé avec des dates-heures qui ont un +5:30 , indiquant le fuseau horaire. La base de données stocke les heures en UTC.

Le comportement inattendu est que lorsque l'objet est à nouveau sérialisé ultérieurement, les dates-heures sont toutes au format UTC avec un Z . Selon la documentation de Django sur le fuseau horaire actuel et par défaut, je m'attendrais à ce que le sérialiseur convertisse les datetimes dans le fuseau horaire actuel, qui est par défaut le fuseau horaire par défaut, qui est défini par TIME_ZONE = 'Asia/Kolkata' .

Est-ce que j'ai raté quelque chose ?

Needs further review

Commentaire le plus utile

Apprécierait également cette fonctionnalité. Je pense que le paramètre USE_TZ doit être respecté et que les valeurs doivent être converties, de manière similaire au comportement standard de Django.

Tous les 34 commentaires

J'ai posé une question sur le débordement de la pile et la conclusion est que le fuseau horaire actuel n'est utilisé que pour les datetimes fournies par l'utilisateur, car la datetime est sérialisée telle quelle après validation (c'est-à-dire après création ou mise à jour). D'après la documentation de Django, j'ai du mal à croire qu'il s'agit d'un comportement intentionnel, mais il s'agit d'un problème avec Django lui-même plutôt qu'avec Rest Framework, je vais donc clore ce problème.

Salut @tomchristie ,

Je pense que c'est un bug du DRF.
À mon humble avis, l'utilisation naturelle de DRF est de l'utiliser comme une sortie comme un modèle Django.
Dans un template django le fuseau horaire est correctement affiché, pourquoi le sérialiseur drf ne respecte pas le paramètre de fuseau horaire django ?

Salut,

en regardant le code dans fields.py à la classe DatetimeField , je découvre que le réglage du fuseau horaire de Django n'est bien pris en compte que pour la valeur interne ( to_internal_value() ).

J'ai vu cela en utilisant le sérialiseur de modèle comme ceci:

class MyModelSerializer(ModelSerializer):

    class Meta:
        model = MyModel
        depth = 1
        fields = ('some_field', 'my_date_time_field')

pour le champ my_date_time_field (que je pense que c'est un DateTimeField :) ) le fuseau horaire pour la représentation est None par défaut ( to_representation() ).

En d'autres termes, si je ne me trompe pas, le fuseau horaire Django n'est pris en compte que lorsque la valeur est écrite dans le backend de stockage.

À mon humble avis, je pense que la valeur de retour de to_representation() devrait ressembler à ceci :
return self.enforce_timezone(value).strftime(output_format)
selon le paramètre Django USE_TZ .

J'écris une demande de tirage pour cela.

Ciao
Valentino

@vpistis Il serait plus facile d'examiner cela si vous pouviez donner un exemple du comportement de l'API que vous voyez actuellement et de ce que vous vous attendez à voir (plutôt que de discuter d'abord des aspects de la mise en œuvre)

Définissez votre TIME_ZONE = 'Asia/Kolkata' et créez un sérialiseur de modèle avec un seul champ datetime, appointment .

Lors de la création/mise à jour, envoyez :

{
    "appointment": "2016-12-19T10:00:00"
}

et reviens :

{
    "appointment": "2016-12-19T10:00:00+5:30"
}

Mais si vous récupérez ou répertoriez cet objet, vous obtenez :

{
    "appointment": "2016-12-19T04:30:00Z"
}

Lors de la création, si le Z n'est pas spécifié, DRF suppose que le client utilise le fuseau horaire spécifié par TIME_ZONE et renvoie une heure formatée selon ce fuseau horaire (en ajoutant le +5:30 à la fin, ce qui serait en fait invalide s'il s'agissait d'une heure fournie par le client). Cela aurait probablement plus de sens si les accès futurs renvoyaient également une heure formatée dans ce fuseau horaire, de sorte que la réponse lors de la récupération/de la liste était la même que lors de la création/mise à jour.

Il y a aussi la question de savoir s'il faut retourner les heures dans le fuseau horaire configuré lorsque le Z de fin est fourni lors de la création/mise à jour, de sorte que l'envoi :

{
    "appointment": "2016-12-19T04:30:00Z"
}

Retour:

{
    "appointment": "2016-12-19T10:00:00+5:30"
}

Je serais pour cela, car cela maintient la réponse cohérente avec la réponse pour les listes/récupérations.

Une autre option serait de toujours renvoyer les heures UTC, même lors de la création/mise à jour, mais je trouve cela moins utile. Quoi qu'il en soit, avoir des fuseaux horaires cohérents serait préférable à la situation 50/50 que nous connaissons actuellement.

Définissez votre TIME_ZONE = 'Asia/Kolkata' et créez un sérialiseur de modèle avec un seul champ datetime, rendez-vous.

Lors de la création/mise à jour, envoyez :

{
"rendez-vous": "2016-12-19T10:00:00"
}
et reviens :

{
"rendez-vous": "2016-12-19T10:00:00+5:30"
}
Mais si vous récupérez ou répertoriez cet objet, vous obtenez :

{
"rendez-vous": "2016-12-19T04:30:00Z"
}

merci @jonathan-golorry c'est le comportement exact que je vois réellement.
Pour moi, le comportement devrait être comme ceci (en utilisant l'exemple @jonathan-golorry :) ):

Lors de la création/mise à jour avec le DATETIME_FORMAT par défaut, envoyez :

{
    "appointment": "2016-12-19T10:00:00"
}

et reviens :

{
    "appointment": "2016-12-19T10:00:00+5:30"
}

Si vous récupérez ou répertoriez cet objet , vous obtenez :

{
    "appointment": "2016-12-19T10:00:00+5:30"
}

IMHO devrait peut-être être un paramètre DRF pour gérer ce comportement, par exemple, un paramètre pour forcer le DateTimeField à être représenté avec le fuseau horaire par défaut.

merci beaucoup

L'incohérence entre les différentes actions est le facteur décisif pour la réouverture ici. Je n'avais pas réalisé que c'était le cas. Je m'attendrais à ce que nous utilisions l'UTC partout par défaut, bien que nous puissions le rendre facultatif.

Pour une application Web de production qui utilise DRF 3.4.6, nous avons résolu cette solution de contournement :
https://github.com/vpistis/django-rest-framework/commit/be62db9080b19998d4de3a1f651a291d691718f6

Si quelqu'un souhaite soumettre une demande d'extraction comprenant :

  • inclut simplement les cas de test échoués, ou ...
  • cas de test + correctif

ce serait le bienvenu.

J'ai écrit du code à tester, mais je ne sais pas comment utiliser les cas de test drf. Je ne sais pas comment gérer les paramètres de Django pour modifier le fuseau horaire et d'autres paramètres au moment de l'exécution.
S'il vous plaît, associez-moi un exemple ou un guide spécifique.

merci

Si vous souhaitez modifier les paramètres de test globaux, ils se trouvent ici .

Si vous essayez de remplacer les paramètres pendant les tests, vous pouvez utiliser le décorateur override_settings .

Apprécierait également cette fonctionnalité. Je pense que le paramètre USE_TZ doit être respecté et que les valeurs doivent être converties, de manière similaire au comportement standard de Django.

La première étape ici serait d'écrire un cas de test défaillant que nous pouvons ajouter à la suite de tests actuelle.
La prochaine étape serait de commencer un correctif pour ce cas :)

Salut,
pour moi, c'est un cas de test pour cette fonctionnalité

class TestDateTimeFieldTimeZone(TestCase):
    """
    Valid and invalid values for `DateTimeField`.
    """
    from django.utils import timezone

    valid_inputs = {
        '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.get_default_timezone()),
        '2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.get_default_timezone()),
        '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.get_default_timezone()),
        datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.get_default_timezone()),
        datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00,
                                                                                        tzinfo=timezone.get_default_timezone()),
        # Django 1.4 does not support timezone string parsing.
        '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.get_default_timezone())
    }
    invalid_inputs = {}
    outputs = {
        # This is not simple, for now I suppose TIME_ZONE = "Europe/Rome"
        datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.get_default_timezone()): '2001-01-01T13:00:00+01:00',
        datetime.datetime(2001, 1, 1, 13, 00, ): '2001-01-01T13:00:00+01:00',
    }

    field = serializers.DateTimeField()

Dans mon fork, j'utilise une astuce pour obtenir les heures dans le bon fuseau horaire mon 3.6.2_tz_fix .

J'espère que cette aide :)

Je vois que c'est fermé mais j'exécute drf 3.6.3 et dans ma base de données postgres j'ai cet horodatage "2017-07-12 14:26:00-06" mais quand j'obtiens les données en utilisant postman, j'obtiens cet "horodatage" : "2017-07-12T20:26:00Z". J'ai l'impression qu'il s'ajoute aux -06 heures.

Mes paramètres Django utilisent tzlocal pour définir le fuseau horaire TIME_ZONE = str(get_localzone()) . Le fuseau horaire est donc défini au démarrage.

J'utilise un modèle de baseSerializer

class SnapshotSerializer(serializers.ModelSerializer):
    class Meta:
        model = Snapshot
        resource_name = 'snapshot' 
        read_only_fields = ('id',)
        fields = ('id', 'timestamp', 'snapshot')

Est-ce que j'ai raté quelque chose ?

pas fermé, il est toujours ouvert :)
le "bouton fermé" rouge que vous voyez est pour le problème de référence.
...et tu as raison, le "bug" est toujours là ;(
Le jalon est modifié de 3.6.3 à 3.6.4.

Ah d'accord. Merci!

Le 12 juillet 2017 17:10, notifications "Valentino Pistis"@
a écrit:

pas fermé, il est toujours ouvert :)
le "bouton fermé" rouge que vous voyez est pour le problème de référence.
...et tu as raison, le "bug" est toujours là ;(
Le jalon est modifié de 3.6.3 à 3.6.4.

-
Vous recevez ceci parce que vous avez commenté.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/encode/django-rest-framework/issues/3732#issuecomment-314923582 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AIMBTcx-6PPbi_SOqeLCjeWV1Rb59-Ohks5sNVJ0gaJpZM4G0aRE
.

Salut,

Je ne sais pas si cela est totalement lié à la question d'origine, mais DRF devrait-il honorer les modifications de fuseau horaire définies, conformément à https://docs.djangoproject.com/en/1.11/ref/utils/#django. utils.timezone.activate ?

J'ai un système dans lequel mes utilisateurs sont associés à un fuseau horaire et l'API reçoit des dates-heures naïves. Je m'attends à pouvoir convertir ces datetimes dans le fuseau horaire de l'utilisateur actuel, mais je remarque que ../rest_framework/fields.py applique le fuseau horaire par défaut (c'est-à-dire celui du fichier de configuration django :

    def enforce_timezone(self, value):
        field_timezone = getattr(self, 'timezone', self.default_timezone())

        if (field_timezone is not None) and not timezone.is_aware(value):
            try:
                return timezone.make_aware(value, field_timezone)

[...]

    def default_timezone(self):
        return timezone.get_default_timezone() if settings.USE_TZ else None

Cela devrait-il vraiment utiliser timezone.get_current_timezone() comme préférence au cas où l'application aurait défini une dérogation, comme dans mon cas ?

Salut @RichardForshaw, cela semble être un problème distinct, mais le même terrain de jeu certainement.

Si nous pouvons obtenir un ensemble décent de cas de test couvrant le comportement attendu, je pense que nous envisagerions certainement un PR ici.

Ma première pensée est au-delà de cela de m'assurer que vous envoyez les informations de fuseau horaire à l'API, plutôt que de vous fier au fuseau horaire configuré du serveur. Au-delà de cela, je suis également enclin à penser que vous devez être prêt à localiser une heure sur le client.

Mais oui, il y a ici une incohérence à corriger. (Ai-je mentionné PRs Welcome ? 🙂)

carltongibson/django-filter#750 devrait être pertinent ici. J'ai initialement basé la gestion des fuseaux horaires de django-filter sur DRF, de sorte que les modifications apportées à 750 pourraient facilement être appliquées ici.

Désolé pour mon novice, mais quel est exactement le problème ici ? L'horodatage de ma base de données psql est correct et Django est configuré pour utiliser le bon fuseau horaire. Existe-t-il des paramètres DRF pour qu'il ne convertisse pas les horodatages ?

Salut @michaelaelise — si vous regardez l'exemple des données (près du haut) :

  1. Ils envoient une date/heure sans informations sur le fuseau horaire. (C'est un mauvais coup dans mon livre.)
  2. Le serveur applique son fuseau horaire local et il revient comme ça ( +5:30 dans ce cas)
  3. Mais plus tard, lorsque vous le récupérez, il revient en UTC ( Z , pour "Zulu" je suppose).

Il n'y a pas de problème réel dans l'hypothèse où votre client se chargera de la conversion de fuseau horaire pour vous . (Sauf peut-être No1, car qui peut dire que votre client a le même paramètre de fuseau horaire que votre serveur...?)

Mais c'est une petite incohérence, sûrement 2 et 3 devraient avoir le même format ? (Même si un client accepte correctement l'une ou l'autre comme valeurs équivalentes.)

Je suis enclin à fermer ça.

  • Il n'y a pas d'erreur logique ici.

    • Ce sont les mêmes temps : "2016-12-19T10:00:00+5:30" et "2016-12-19T04:30:00Z" — dans une certaine mesure, qui se soucie de la façon dont ils reviennent ?

  • Ainsi, ce n'est pas quelque chose que je peux justifier d'allouer du temps vraiment.
  • Le billet a 2 ans et personne n'a offert de RP.

Je suis heureux de regarder un PR, mais je ne suis pas sûr de vouloir vraiment considérer cela comme un _Problème ouvert_.

Oh, je n'avais pas réalisé que dans mon message d'origine dans ce fil de discussion, le "Z" signifiait cela.
Donc, DRF convertit la date et l'heure consciente en UTC pour qu'elle soit gérée par l'interface utilisateur/l'appelant ?
Merci pour cette précision.

Une dernière chose.
Que se passe-t-il si nous souhaitons que "2016-12-19T10:00:00+5:30" soit renvoyé parce que nous interrogeons des appareils dans des fuseaux horaires différents.
Serait-ce un paramètre "RETURN_DATETIME_WITH_TIMEZONE" ?

Nous utilisons django/drf sur des appareils périphériques. Ainsi, toutes les dates et heures insérées ne se soucient pas de savoir si elles sont naïves ou non, car le fuseau horaire du périphérique Edge est configuré et le champ postgres sera toujours précis pour la date et l'heure de ce périphérique.

Le serveur cloud dans le scénario actuel aurait alors besoin de connaître le fuseau horaire de chaque appareil, ce sera probablement le cas, ce qui déplacera simplement le travail de django/drf vers l'application cloud à gérer.

En supposant que USE_TZ, DRF renvoie déjà les heures de date avec les informations de fuseau horaire. Donc, il fait déjà ce dont vous avez besoin là-bas.

Le seul problème ici est de savoir si le même DT est formaté comme dans un fuseau horaire ou un autre. (Mais c'est toujours la même heure.)

@carltongibson

Il n'y a pas d'erreur logique ici.
Ce sont les mêmes heures : "2016-12-19T10:00:00+5:30" et "2016-12-19T04:30:00Z" - dans une certaine mesure, qui se soucie de la façon dont ils reviennent ?

IMHO c'est le problème : la chaîne retournée !
J'utilise les paramètres de fuseau horaire de Django et tous les modèles renvoient l'heure correcte "2016-12-19T10:00:00+5:30" comme prévu, mais pas DRF. DRF renvoie "2016-12-19T04:30:00Z" .
Dans le client, qui consomme mes API REST, il n'y a pas de logique, pas de conversions d'heures ou d'interprétation de chaîne de date/heure.
En d'autres termes, je m'attends à ce que la date/heure d'une réponse DRF soit identique à la "réponse" du modèle Django : le serveur prépare toutes les données pour le client et le client ne fait que les montrer.

Quoi qu'il en soit, merci beaucoup pour votre patience, votre soutien et votre excellent travail pour ce projet fantastique !

@vpistis mon argument ici est simplement que la date représentée est correcte, seule la représentation n'est pas attendue. Dès que vous analysez cela vers une date native, quelle que soit la manière dont votre langue gère cela, il n'y a aucune différence.

Je m'attendrais à ce que les utilisateurs analysent la chaîne de date en une date, mais la langue de leur client le permet, plutôt que de consommer la chaîne brute.

J'accepte si vous consommez la chaîne brute, vos attentes ne seront pas satisfaites ici. (Mais ne faites pas ça : imaginez si nous envoyions des horodatages UNIX ; il n'y a aucun moyen que vous les consommiez bruts. Convertissez-les en un objet Date approprié, quel qu'il soit dans la langue de votre client.)

Je suis vraiment heureux de prendre un PR à ce sujet. (je ne l'ai pas encore fermé !)

Mais cela fait près de deux ans depuis le signalement et neuf mois depuis le premier commentaire (le vôtre, un an plus tard). Personne ne nous a même donné un cas de test raté. Cela ne peut être si important pour personne. En tant que tel, il est difficile de lui allouer du temps.

(En tant que tel, je suis enclin à le fermer sur la base que nous prendrons un PR si jamais il se présente)

Salut à tous, cela devrait être corrigé par #5408. Si vous avez le temps d'installer la branche et de vérifier que tout fonctionne comme prévu, ce serait fantastique. Merci!

Je pense que le problème a été réintroduit en quelque sorte :

Lorsque j'ai changé la TZ par défaut d'UTC à Europe/Amsterdam, l'un des tests a échoué et j'ai remarqué que DRF se sérialise en quelque chose de différent de la TZ par défaut

edit: le problème était lié au test/configuration d'usine.


Testez la configuration ci-dessous.

maquette

class Something(StampedModelMixin):
    MIN_VALUE = 1
    MAX_VALUE = 500

    id = models.BigAutoField(primary_key=True)  # pylint: disable=blacklisted-name
    product_id = models.BigIntegerField(db_index=True)
    start_time = models.DateTimeField()
    end_time = models.DateTimeField()
    percentage = models.IntegerField()
    enabled = models.IntegerField()

usine

class SomethingFactory(factory.django.DjangoModelFactory):
    """ Base Factory to create records for Something

    """
    start_time = factory.Faker('date_time', tzinfo=get_default_timezone())
    end_time = factory.Faker('date_time', tzinfo=get_default_timezone())
    percentage = factory.Faker('random_int', min=1, max=500)
    enabled = factory.Faker('random_element', elements=[0, 1])

    class Meta:  # pylint: disable=missing-docstring
        model = Something

Test de l'unité

class TestSomething:
    def test__get__empty(self):
        # preparation of data
        series = SeriesFactory.create(product_id=2)
        SomethingFactory.create_batch(3, product_id=1)

        # prepare request params
        url = reverse('series-somethings', kwargs={'pk': series.pk})

        # call the endpoint
        response = self.client.get(url)

        # asserts
        assert response.data == []

    def test__get__single(self):
        # preparation of data
        series = SeriesFactory.create(product_id=1)
        old_somethings = SomethingFactory.create_batch(1, product_id=1)

        # prepare request params
        url = reverse('series-somethings', kwargs={'pk': series.pk})

        # call the endpoint
        response = self.client.get(url)

        # asserts
        assert SomethingSerializer(old_somethings, many=True).data == response.data

vue

class SomethingElseView(APILogMixin, ModelViewSet):
    @action(detail=True, methods=['get'])
    def somethings(self, request, pk=None):
        """ GET endpoint for Somethings

        .. seealso:: :func:`rest_framework.decorators.action`
        """
        otherthings = self.get_object()
        something_qs = Something.objects.all()
        something_qs = something_qs.filter(product_id=otherthings.product_id)
        serializer = self.something_serializer_class(something_qs, many=True)
        return Response(serializer.data)

Sérialiseur

class SomethingSerializer(serializers.ModelSerializer):

    class Meta:
        model = Something
        list_serializer_class = SomethingListSerializer
        fields = '__all__'
        extra_kwargs = {
            'percentage': {'min_value': Something.MIN_VALUE,
                           'max_value': Something.MAX_VALUE}
        }
        read_only_fields = ('id',
                            'ts_activated',
                            'ts_created',
                            'ts_updated')

Tester le résultat ipdb

ipdb> old_somethings
[{'product_id': 1, 'start_time': datetime.datetime(2011, 7, 13, 1, 10, 33, tzinfo=<DstTzInfo 'Europe/Amsterdam' LMT+0:20:00 STD>), 'end_time': datetime.datetime(2003, 3, 10, 9, 31, tzinfo=<DstTzInfo 'Europe/Amsterdam' LMT+0:20:00 STD>), 'percentage': 103, 'enabled': 0}]
ipdb> response.data
[OrderedDict([('id', 1), ('ts_created', '2019-02-27 14:16:33'), ('ts_updated', '2019-02-27 14:16:33'), ('product_id', 1), ('start_time', '2011-07-13 02:50:33'), ('end_time', '2003-03-10 10:11:00'), ('percentage', 103), ('enabled', 0)])]

Résultat du test

E       AssertionError: assert [OrderedDict(...nabled', 0)])] == [OrderedDict([...nabled', 0)])]
E         At index 0 diff: OrderedDict([('id', 1), ('ts_created', '2019-02-27 14:38:15'), ('ts_updated', '2019-02-27 14:38:15'), ('product_id', 1), ('start_time', '2011-07-13 01:10:33'), ('end_time', '2003-03-10 09:31:00'), ('percentage', 103), ('enabled', 0)]) != OrderedDict([('id', 1), ('ts_created', '2019-02-27 14:38:15'), ('ts_updated', '2019-02-27 14:38:15'), ('product_id', 1), ('start_time', '2011-07-13 02:50:33'), ('end_time', '2003-03-10 10:11:00'), ('percentage', 103), ('enabled', 0)])
E         Full diff:
E         - [OrderedDict([('id', 1), ('ts_created', '2019-02-27 14:38:15'), ('ts_updated', '2019-02-27 14:38:15'), ('product_id', 1), ('start_time', '2011-07-13 01:10:33'), ('end_time', '2003-03-10 09:31:00'), ('percentage', 103), ('enabled', 0)])]
E         ?                                                                                                                                                       ^^^^^^^^^^                          ^^^^^^^^^^^
E         + [OrderedDict([('id', 1), ('ts_created', '2019-02-27 14:38:15'), ('ts_updated', '2019-02-27 14:38:15'), ('product_id', 1), ('start_time', '2011-07-13 02:50:33'), ('end_time', '2003-03-10 10:11:00'), ('percentage', 103), ('enabled', 0)])]
E         ?

Empiler:

Django==2.0.10
djangorestframework==3.9.0
factory-boy==2.11.1
Faker==0.8.18
pytz==2018.9

Salut @diegueus9 - pourriez-vous réduire cela à un cas de test plus simple ? Vous comparez de fausses données sérialisées aux données de réponse d'une vue. La valeur réelle attendue n'est donc pas claire. Je recommanderais de comparer un résultat à une valeur codée en dur. par exemple,

assert SomethingSerializer(old_somethings, many=True).data == {'blah':'blah'}

@rpkilby merci pour votre réponse. C'était mon erreur, le problème avec mes tests unitaires est que j'utilisais factory-boy/faker pour ça sans rafraîchir depuis DB, d'où la différence, je viens d'ajouter

    for old_something in old_somethings:
        old_something.refresh_from_db()

Dois-je supprimer mon commentaire précédent ou dois-je le laisser au cas où quelqu'un d'autre éprouverait le même faux positif ?

Salut @diegueus9 , pas de soucis - je viens de cacher le code dans une balise details .

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

doctorallen picture doctorallen  ·  3Commentaires

gabn88 picture gabn88  ·  3Commentaires

tomchristie picture tomchristie  ·  3Commentaires

thnee picture thnee  ·  3Commentaires

akhilputhiry picture akhilputhiry  ·  4Commentaires