Ищите лучший способ делать уведомления на основе часовых поясов в Django 3 - PullRequest
0 голосов
/ 21 января 2020

Я хотел бы отправить электронное письмо всем пользователям, которые не вошли в дневник сегодня. Я хотел бы дать им до 8 вечера по местному времени, чтобы войти в запись в журнале, и все получилось, но код неопрятен, и мне интересно, есть ли более эффективный способ достижения sh моей цели.

В настоящее время я сохраняю смещение часового пояса пользователя при регистрации.

<style>
  #clientTimezone {
    opacity: 0;
    width: 0;
    float: left;
  }
</style>

....

<form class="signup form-default row" id="signup_form" method="post" action="{% url 'account_signup' %}">
  {% csrf_token %}
  <input type="text" name="client_timezone" id="clientTimezone" required />

....

<script>
  $(document).ready(function(){
    var d = new Date();
    $('#clientTimezone').val(moment().format('ZZ'))
  })
</script>

А затем код электронной почты находится в моей команде управления dailyreminder.py, которая срабатывает каждые 10 минут. Я хотел бы продолжить делать это без сельдерея, если это возможно.

import arrow
from datetime import datetime, timedelta

from django.utils import timezone
from django.core.management.base import BaseCommand, CommandError
from django.db.models import Q
from django.core.mail import send_mail, send_mass_mail, EmailMultiAlternatives
from django.conf import settings

from dashboard.models import Entry
from dashboard.emails import email_daily_reminder
from redwoodlabs.models import UserProfile
from identity.strings import GLOBAL_STRINGS

LOCAL_TIME_TO_EMAIL = 20 # 8:00 PM
MAX_LOCAL_TIME_TO_EMAIL = 24 # 12:00 AM
USER_BATCH_SIZE = 10

class Command(BaseCommand):
    help = 'Email users daily reminder to reflect'

    def handle(self, *args, **options):

        # UserProfile model has last_notified (datetimefield) and timezone (char field)

        # get current time in utc
        time_now = arrow.utcnow()

        # Get timezone where it's 8PM
        start_tz = LOCAL_TIME_TO_EMAIL - time_now.hour
        end_tz = MAX_LOCAL_TIME_TO_EMAIL - time_now.hour

        # Get a list of timezones where it is between 8pm to 12am
        timezones = list()
        for tz in range(start_tz, end_tz):
            if tz > 14:
                tz = 24 - tz
                timezones.append('-' + str(tz).zfill(2) + '00')
            else:
                timezones.append('+' + str(tz).zfill(2) + '00')

        time_now_local = time_now.replace(minute=0, second=0)
        time_yesterday = time_now.shift(days=-1)

        # Get all users with the ff cases:
        # - who have not been notified today
        # - who have not been notified at all
        # - who have not been notified in the last 24hrs
        users_to_email = UserProfile.objects.filter(Q(last_notified__isnull=True) |
            (Q(timezone__in=timezones) & Q(last_notified__lt=time_now_local.datetime)) |
            Q(last_notified__lt=time_yesterday.datetime)
        ).exclude(timezone__isnull=True)[:USER_BATCH_SIZE]

        for profile in users_to_email:
            user_time_now = time_now.to(profile.timezone)
            user_today = user_time_now.floor('day')
            last_notified_day = arrow.get(profile.last_notified).to(profile.timezone)

            # Send email if last_notified was before today
            if not profile.last_notified or last_notified_day.datetime < user_today.datetime:
                self.stdout.write('Notifying %s' % profile)

                email_sent = email_daily_reminder(profile.user, user_time_now.date())

                # save somewhere they've been notified today?
                self.stdout.write(self.style.SUCCESS('Notified %s' % profile.user.email))
                profile.last_notified = user_time_now
                profile.save()

Это работает (или, по крайней мере, пока), но я хотел спросить, есть ли лучший способ сделать это (особенно logi c вокруг часовых поясов), потому что я, скорее всего, снова буду использовать этот код в другом проекте и хотел бы изучить лучшие практики для него. Это кажется как у Django есть лучший способ сделать это.

1 Ответ

1 голос
/ 22 января 2020

Django имеет множество функций базы данных , которые упростят ваши вычисления. По сути, вы можете попробовать аннотировать свой набор запросов местным временем / часом каждого UserProfile, а затем выполнить все необходимые вычисления на уровне БД, не создавая список часовых поясов. Нечто похожее на это может работать:

from django.db.models import F
from django.db.models.functions import Now
import datetime

users_to_email = UserProfile.objects.exclude(
    timezone__isnull=True
).annotate(
    local_time=ConvertToTimezone(Now(), 'timezone') # Annotate with users' local time
).filter(
    local_time__hour__gte=LOCAL_TIME_TO_EMAIL # Filter to only include users whose local time is past 20 hr
).filter(
    Q(last_notified__isnull=True) | # Not notified at all
    Q(last_notified__date__lt=F('local_time__date')) # Not notified today
).distinct()[:USER_BATCH_SIZE]

for profile in users_to_email:
    self.stdout.write('Notifying %s' % profile)
    email_sent = email_daily_reminder(profile.user, user_time_now.date())
    self.stdout.write(self.style.SUCCESS('Notified %s' % profile.user.email))
    profile.last_notified = user_time_now
    profile.save()

Здесь ConvertToTimezone не является функцией db, доступной в Django, но ниже приведена пользовательская функция db, которую я использовал ранее для этой задачи:

from django.db.models import Func, DateTimeField

class ConvertToTimezone(Func):
    """
    Custom SQL expression to convert time to timezone stored in database column
    """

    output_field = DateTimeField()

    def __init__(self, datetime_field, timezone_field, **extra):
        expressions = datetime_field, timezone_field
        super(ConvertToTimezone, self).__init__(*expressions, **extra)

    def as_sql(self, compiler, connection, fn=None, template=None, arg_joiner=None, **extra_context):
        params = []
        sql_parts = []
        for arg in self.source_expressions:
            arg_sql, arg_params = compiler.compile(arg)
            sql_parts.append(arg_sql)
            params.extend(arg_params)

        return "%s AT TIME ZONE %s" % tuple(sql_parts), params

Я не тестировал этот код, поэтому возможно, что он не сработает сразу, но должен дать вам несколько советов по использованию функций базы данных Django, чтобы упростить ваш код здесь.

...