# -*- coding: utf-8 -*- import time as t from datetime import date, datetime, timedelta, time import six from dateutil import parser, relativedelta from django.utils import timezone from graphql import GraphQLArgument, GraphQLString from .base import BaseExtraGraphQLDirective from ..base_types import CustomDateFormat __all__ = ("DateGraphQLDirective",) DEFAULT_DATE_FORMAT = "%d %b %Y %H:%M:%S" FORMATS_MAP = { # Year "YYYY": "%Y", "YY": "%y", # Week of year "WW": "%U", "W": "%W", # Day of Month # 'D': '%-d', # Platform specific "DD": "%d", # Day of Year # 'DDD': '%-j', # Platform specific "DDDD": "%j", # Day of Week "d": "%w", "ddd": "%a", "dddd": "%A", # Month # 'M': '%-m', # Platform specific "MM": "%m", "MMM": "%b", "MMMM": "%B", # Hour # 'H': '%-H', # Platform specific "HH": "%H", # 'h': '%-I', # Platform specific "hh": "%I", # Minute # 'm': '%-M', # Platform specific "mm": "%M", # Second # 's': '%-S', # Platform specific "ss": "%S", # AM/PM # 'a': '', "A": "%p", # Timezone "ZZ": "%z", "z": "%Z", } def str_in_dict_keys(s, d): for k in d: if s in k: return True return False def _combine_date_time(d, t): if (d is not None) and (t is not None): return datetime(d.year, d.month, d.day, t.hour, t.minute, t.second) return None def _parse(partial_dt): """ parse a partial datetime object to a complete datetime object """ dt = None try: if isinstance(partial_dt, datetime): dt = partial_dt if isinstance(partial_dt, date): dt = _combine_date_time(partial_dt, time(0, 0, 0)) if isinstance(partial_dt, time): dt = _combine_date_time(date.today(), partial_dt) if isinstance(partial_dt, (int, float)): dt = datetime.fromtimestamp(partial_dt) if isinstance(partial_dt, (str, bytes)): dt = parser.parse(partial_dt, default=timezone.now()) if dt is not None and timezone.is_naive(dt): dt = timezone.make_aware(dt) return dt except ValueError: return None def _format_relativedelta(rdelta, full=False, two_days=False, original_dt=None): if not isinstance(rdelta, relativedelta.relativedelta): raise ValueError("rdelta must be a relativedelta instance") keys = ("years", "months", "days", "hours", "minutes", "seconds") result = [] flag = None if two_days and original_dt: if rdelta.years == 0 and rdelta.months == 0: days = rdelta.days if days == 1: return None, "Tomorrow" if days == -1: return None, "Yesterday" if days == 0: full = False else: return None, original_dt.strftime("%b %d, %Y") else: return None, original_dt.strftime("%b %d, %Y") for k, v in rdelta.__dict__.items(): if k in keys and v != 0: if flag is None: flag = True if v > 0 else False key = k abs_v = abs(v) if abs_v == 1: key = key[:-1] if not full: return flag, "{} {}".format(abs_v, key) else: result.append("{} {}".format(abs_v, key)) if len(result) == 0: return None, "Now" if two_days else None, "just now" if len(result) > 1: temp = result.pop() result = "{} and {}".format(", ".join(result), temp) else: result = result[0] return flag, result def _format_time_ago(dt, now=None, full=False, ago_in=False, two_days=False): if not isinstance(dt, timedelta): if now is None: now = timezone.localtime( timezone=timezone.get_fixed_timezone(-int(t.timezone / 60)) ) original_dt = dt dt = _parse(dt) now = _parse(now) if dt is None: raise ValueError( "The parameter `dt` should be datetime timedelta, or datetime formatted string." ) if now is None: raise ValueError( "the parameter `now` should be datetime, or datetime formatted string." ) result = relativedelta.relativedelta(dt, now) flag, result = _format_relativedelta(result, full, two_days, original_dt) if ago_in and flag is not None: result = "in {}".format(result) if flag else "{} ago".format(result) return result def _format_dt(dt, format="default"): if not dt: return None format_lowered = format.lower() if format_lowered == "default": return dt.strftime(DEFAULT_DATE_FORMAT) if format_lowered == "time ago": return _format_time_ago(dt, full=True, ago_in=True) if format_lowered == "time ago 2d": return _format_time_ago(dt, full=True, ago_in=True, two_days=True) if format_lowered == "iso": return dt.strftime("%Y-%b-%dT%H:%M:%S") if format_lowered in ("js", "javascript"): return dt.strftime("%a %b %d %Y %H:%M:%S") if format in FORMATS_MAP: return dt.strftime(FORMATS_MAP[format]) else: temp_format = "" translate_format_list = [] for char in format: if not char.isalpha(): if temp_format != "": translate_format_list.append(FORMATS_MAP.get(temp_format, "")) temp_format = "" translate_format_list.append(char) else: if str_in_dict_keys("{}{}".format(temp_format, char), FORMATS_MAP): temp_format = "{}{}".format(temp_format, char) else: if temp_format != "": if temp_format in FORMATS_MAP: translate_format_list.append( FORMATS_MAP.get(temp_format, "") ) else: return None if str_in_dict_keys(char, FORMATS_MAP): temp_format = char else: return None if temp_format != "": if temp_format in FORMATS_MAP: translate_format_list.append(FORMATS_MAP.get(temp_format, "")) else: return None format_result = "".join(translate_format_list) if format_result: return dt.strftime("".join(translate_format_list)) return None class DateGraphQLDirective(BaseExtraGraphQLDirective): """ Format the date from resolving the field by dateutil module. """ @staticmethod def get_args(): return { "format": GraphQLArgument( type_=GraphQLString, description="A format given by dateutil module" ) } @staticmethod def resolve(value, directive, root, info, **kwargs): format_argument = [ arg for arg in directive.arguments if arg.name.value == "format" ] format_argument = format_argument[0] if len(format_argument) > 0 else None custom_format = format_argument.value.value if format_argument else "default" dt = _parse(value) try: result = _format_dt(dt, custom_format) if isinstance(value, six.string_types): return result or value return CustomDateFormat(result or "INVALID FORMAT STRING") except ValueError: return CustomDateFormat("INVALID FORMAT STRING")