├── README.md ├── LICENSE ├── datetools.py └── db_tools.py /README.md: -------------------------------------------------------------------------------- 1 | # mjtb-django-tools 2 | Useful Django tools for enhancing the function of Django 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dr Michael Brooks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /datetools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | .d8888b. d8b 888 5 | d88P Y88b Y8P 888 6 | Y88b. 888 7 | "Y888b. 8888b. 88888b. 888 888888 888 888 8 | "Y88b. "88b 888 "88b 888 888 888 888 9 | "888 .d888888 888 888 888 888 888 888 10 | Y88b d88P 888 888 888 888 888 Y88b. Y88b 888 11 | "Y8888P" "Y888888 888 888 888 "Y888 "Y88888 12 | 888 13 | Y8b d88P 14 | "Y88P" 15 | The Sanity Project - Code to make life easy 16 | 17 | DJANGO TOOLS - Date tools 18 | 19 | @requires: python-dateutil 20 | @requires: django>=1.4 21 | 22 | 23 | date_now(): 24 | Gives the current date & time in a timezone aware datetime 25 | 26 | date_now(format="%Y-%m-%d %H:%M"): 27 | Gives the current date & time as a string according to the format you specify using 28 | strftime syntax. 29 | e.g. "2017-04-12 20:51" 30 | 31 | convert_date(datestr): 32 | Works out what datetime you mean from a string, then returns a timezone aware datetime. 33 | No more mucking about with strptime! 34 | 35 | delta_as_text(datetime1, datetime2) 36 | Works out how long ago datetime1 was compared to datetime2. Expresses this in human- 37 | readable terms. i18n safe. If you leave out datetime2, it will compare datetime1 to now. 38 | e.g. "4 months, 3 days" 39 | 40 | 41 | """ 42 | from __future__ import unicode_literals 43 | from datetime import timedelta, time, datetime as datetime_dumb 44 | from dateutil.parser import parse 45 | from dateutil.relativedelta import relativedelta 46 | from django.utils.datetime_safe import datetime 47 | from django.utils.timezone import utc, make_aware, is_naive, get_current_timezone, localtime 48 | from django.utils.translation import ungettext as _PSTAT 49 | 50 | ISO8601_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S%z" #Unbelievably, %z is supported in strftime but not in strptime... Use convert_date instead of strptime 51 | 52 | 53 | def date_now(local_tz=False, format=None, tz=None): 54 | """ 55 | Fetches the current date in the current timezone at the moment this function is called. 56 | Ensures you always get a timezone-aware time for now(). Call this function to guarantee a true now() timestamp 57 | 58 | @param local_tz: or "local" or instance: What timezone to give the now() value in. 59 | If True or 1 or "local": uses locale's timezone 60 | if a Timezone instance, will use that Timezone 61 | @keyword format: A strftime representation of what to cast the date into. This means you'll get a string back!! 62 | If you supply "ISO", will return it in ISO8601 format (%Y-%m-%dT%H:%M:%S%z), cross-platform capable 63 | 64 | @return: for now() if format not supplied. Timezone aware 65 | representation of now(), if format IS supplied. Will report the timezone if you ask for it in your formatting rules (e.g. %Z or %z) 66 | """ 67 | #The definitive way to get now() in Django as a TZ aware: 68 | now = datetime_dumb.utcnow().replace(tzinfo=utc) 69 | 70 | #In the spirit of consistency, this also takes "tz" as an expression for your timezone 71 | if not local_tz and tz is not None: 72 | local_tz = tz 73 | 74 | #Has a local_tz expression of a timezone been given? 75 | if local_tz and str(local_tz) not in ("utc", "UTC"): #Yes there's a local timezone indicated 76 | #What timezone are they trying to get at? 77 | if str(local_tz).lower() == "local" or local_tz in (True, 1, "true"): #Means they want the local timezone 78 | target_tz = get_current_timezone() 79 | else: #Means they want some other timezone: 80 | target_tz = local_tz 81 | 82 | #Now convert to the relevant timezone 83 | if is_naive(now): #Ensures we give a local timezone-aware dt whatever happens 84 | now = make_aware(now, target_tz) #Wasn't aware. Is now to the local tz 85 | else: 86 | now = localtime(now, target_tz) 87 | 88 | #If format flag set, strftime it to that format: 89 | if format: 90 | if str(format).lower() in ("iso","iso8601"): 91 | format = ISO8601_DATE_FORMAT 92 | now = now.strftime(format) 93 | return now 94 | 95 | 96 | def convert_date(datestr, tz=False): 97 | """ 98 | Takes datestr, parses it into a date if possible, then makes it timezone aware if lacking 99 | NB: it assumes the current timezone if a string that maps to a naive datetime is provided 100 | 101 | @param datestr: or - something which we can try resolve a date from 102 | @keyword tz: or "local" or "utc" - the timezone to assume the date is in. 103 | If already timezone aware, will convert it to that timezone 104 | If none specified, will use LOCALTIME. 105 | 106 | @return: timezone-aware date, in the timezone you have specified 107 | """ 108 | try: 109 | if isinstance(datestr,(datetime, datetime_dumb)): #First if the item is already a datetime object, don't try to resolve it!! 110 | datecon = datestr 111 | else: #Is a string, try to resolve it 112 | datecon = parse(datestr, fuzzy=True, default=None) #MJTB - 2017-04-12 - Bugfixed to ensure parser doesn't buff out 113 | if datecon: 114 | #Check if timezone needs to be made aware: 115 | if not tz or tz=="local": #DEFAULT TO LOCAL!! 116 | tz = get_current_timezone() 117 | elif str(tz).lower()=="utc": 118 | tz = utc 119 | if is_naive(datecon): 120 | #Need to make this timezone aware now! 121 | datecon = make_aware(datecon, tz) 122 | else: #Is already timezone aware, so need to convert to the specified timezone 123 | datecon = localtime(datecon, tz) 124 | return datecon 125 | else: 126 | return False 127 | except KeyError: 128 | return False 129 | 130 | 131 | def get_midnight(dt, offset): 132 | """ 133 | Return the UTC time corresponding to midnight in local time for the day specified, offset forward by the specified 134 | number of days (eg. offset=0 -> last midnight, offset=1 -> next midnight etc) 135 | 136 | @param dt: The datetime you are converting to midnight 137 | @return: Aware 138 | """ 139 | current_tz = get_current_timezone() 140 | local = current_tz.normalize(dt.astimezone(current_tz)) # Convert to local time before processing to get next local midnight 141 | o = datetime.combine(local + timedelta(days=offset), time(0, 0, 0)) 142 | return make_aware(o, get_current_timezone()) 143 | 144 | def next_midnight(dt): 145 | """ 146 | Get the next midnight 147 | @param dt: The datetime you are converting to midnight 148 | @return: Aware 149 | """ 150 | return get_midnight(dt, 1) 151 | 152 | def last_midnight(dt): 153 | """ 154 | Get the last midnight 155 | @param dt: The datetime you are converting to midnight 156 | @return: Aware 157 | """ 158 | return get_midnight(dt, 0) 159 | 160 | 161 | def delta_as_text(dt1, dt2=None, tz1="utc", tz2="utc", include="YmdHM", include_zeros=True): 162 | """ 163 | Tells you how old dt1 is compared to dt2 in translated textual format 164 | If dt2 is not given, will assume you mean "now". 165 | 166 | @param dt1: or or expression of datetime 167 | @keyword dt2: or or expression of datetime. If omitted, will use 168 | @keyword tz1: the timezone to either convert dt1 to, or to assume dt1 is in (if naive) 169 | @keyword tz2: the timezone to either convert dt2 to, or to assume dt1 is in (if naive) 170 | @keyword include: a description in strftime nomenclature for what elements to include 171 | Y or y = Years 172 | m or b or B = months 173 | d or D = days 174 | H or h = hours 175 | M or I or i = minutes 176 | 177 | @return: representation of the age of dt1 (e.g. "3 years, 2 hours, 1 month, 5 days") 178 | """ 179 | out_desc = [] #Used to build our output 180 | 181 | # Convert dt1 to timezone-aware datetime 182 | dt1 = convert_date(dt1, tz1) 183 | # Convert dt2 to timezone-aware datetime 184 | if dt2: 185 | dt2 = convert_date(dt2, tz2) 186 | else: 187 | dt2 = date_now(local_tz=tz2) #Gets now() in the specified timezone 188 | 189 | #Calculate the relative delta: 190 | delta = relativedelta(dt2, dt1) 191 | 192 | #Determine what we want 193 | do_years = "Y" in include or "y" in include 194 | do_months = "m" in include or "b" in include or "B" in include #Expressions meaning months 195 | do_days = "d" in include or "D" in include 196 | do_hours = "H" in include or "h" in include 197 | do_minutes = "M" in include or "i" in include or "I" in include 198 | 199 | #Build the string components (yes I'm aware we could reduce this to a loop using getattr() on delta) 200 | if do_years and (include_zeros or delta.years != 0): 201 | time_years = _PSTAT("%(count)d year", "%(count)d years", delta.years) % {"count":delta.years} 202 | out_desc.append(time_years) 203 | if do_months and (include_zeros or delta.months != 0): 204 | time_months = _PSTAT("%(count)d month", "%(count)d months", delta.months) % {"count":delta.months} 205 | out_desc.append(time_months) 206 | if do_days and (include_zeros or delta.days != 0): 207 | time_days = _PSTAT("%(count)d day", "%(count)d days", delta.days) % {"count":delta.days} 208 | out_desc.append(time_days) 209 | if do_hours and (include_zeros or delta.hours != 0): 210 | time_hours = _PSTAT("%(count)d hour", "%(count)d hours", delta.hours) % {"count":delta.hours} 211 | out_desc.append(time_hours) 212 | if do_minutes and (include_zeros or delta.minutes != 0): 213 | time_minutes = _PSTAT("%(count)d minute", "%(count)d minutes", delta.minutes) % {"count":delta.minutes} 214 | out_desc.append(time_minutes) 215 | 216 | output = u", ".join(out_desc) 217 | return output 218 | -------------------------------------------------------------------------------- /db_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mike's Django DB Tools 3 | 4 | Provides several useful tools for performing queries in Django ORM 5 | 6 | @author: Dr Michael J T Brooks 7 | @contact: michael [dot] brooks [at] patientsource.co.uk 8 | @license: MIT (software supplied AS IS. Use at your own risk. Leave these credentials in place.) 9 | @version: 20160504 (May the forth be with you) 10 | """ 11 | from __future__ import unicode_literals 12 | from django.db import models 13 | 14 | class Datediff(models.Func): 15 | """ 16 | Returns the date difference of two database fields, using the database's own arithmatic. 17 | 18 | Ever been annoyed at the inconsistent implementation of calculating date differences in databases? 19 | Well this solves that problem. 20 | 21 | Example usage in Django: 22 | 23 | articles = Articles.objects.filter(author=my_user) 24 | articles = articles.annotate(revision_time=Datediff('date_published','date_started', interval='days')) #Assumes your Articles model has fields "date_started" and "date_published" 25 | 26 | @author: Dr Michael J T Brooks 27 | @contact: michael [dot] brooks [at] patientsource.co.uk 28 | @license: MIT (software supplied AS IS. Use at your own risk. Leave these credentials in place.) 29 | @version: 20160504 (May the forth be with you) 30 | 31 | """ 32 | function = '' #We ignore the function property 33 | interval = 'dd' 34 | param_order = ("date_end", "date_start") #Which order do these appear in our template (necessary to ensure correct order of params passed down to cursor.execute) 35 | 36 | def __init__(self, *expressions, **extra): 37 | """ 38 | Initialise the Datediff abstraction ORM function 39 | 40 | @param expressions[0]: The "to" date 41 | @param expressions[1]: The "from" date 42 | @keyword interval: A string representing the units you want the result in (see SUPPORTED_INTERVALS, below) 43 | 44 | """ 45 | SUPPORTED_INTERVALS = """ 46 | 47 | "years" / "year" / "yy" Years 48 | "months" / "month" / "mm" Months 49 | "days" / "day" / "dd" Days [default] 50 | "weeks" / "week" / "wk" Weeks 51 | "hours" / "hour" / "hr" / "hh" Hours 52 | "minutes" / "minute" / "mins" / "min" / "mi" Minutes 53 | "seconds" / "second" / "secs" / "sec" / "ss" Seconds 54 | 55 | """ 56 | interval = unicode(extra.get('interval','dd')).lower() #Default to days 57 | #Now standardise & sanitise interval: 58 | if interval in ("years", "year", "yy"): 59 | interval = "yy" 60 | elif interval in ("months", "month", "mm"): 61 | interval = "mm" 62 | elif interval in ("days", "day", "dd"): 63 | interval = "dd" 64 | elif interval in ("weeks", "week", "wk"): 65 | interval = "wk" 66 | elif interval in ("hours", "hour", "hr", "hh"): 67 | interval = "hh" 68 | elif interval in ("minutes", "minute", "mins", "min", "mi"): 69 | interval = "mi" 70 | elif interval in ("seconds", "second", "secs", "sec", "ss"): 71 | interval = "ss" 72 | else: 73 | raise ValueError('Datediff() interval "{0}" is not supported! Supported intervals are: {1}'.format(interval, SUPPORTED_INTERVALS)) 74 | self.interval = interval 75 | #Sanitise date expressions 76 | if len(expressions) != 2: 77 | raise ValueError('Datediff must take two expressions corresponding to the dates to calculate the difference between') 78 | # The parent init method takes all *expressions, assumes strings are column names, and parses them into a list, which gets put into self.source_expressions 79 | super(Datediff, self).__init__(output_field=models.IntegerField(), *expressions, **extra) 80 | #Something funny happens to self.source_expressions between here and self.as_sql to inflate them into proper SQL 81 | 82 | def as_sql(self, compiler, connection, function=None, template=None): 83 | """ 84 | Complete override to ensure that our compiled date_end and date_start get flushed into as_sql 85 | """ 86 | connection.ops.check_expression_support(self) 87 | params = [] 88 | date_start_sql, date_start_params = compiler.compile(self.source_expressions[1]) #self.source_expressions has now been inflated to a SQL string unlike in init 89 | date_end_sql, date_end_params = compiler.compile(self.source_expressions[0]) 90 | self.extra['date_start'] = date_start_sql #This simply puts the SQL for date_start into the template, but with %s as params 91 | self.extra['date_end'] = date_end_sql 92 | #To prevent SQLinjection, we must pass user derived values in to cursor.execute as a positional list. We cannot use premature string substitution 93 | #The problem, is that the position of date_end and date_start varies depending on the database and interval we are using!!! Thus we use param_order to say what order to stick our params in 94 | params_dict = { 95 | 'date_start':date_start_params, 96 | 'date_end':date_end_params 97 | } #Used to lookup params 98 | for params_for in self.param_order: 99 | params.extend(params_dict[params_for]) #Extend our list of params in the order date_start and date_end appear 100 | # 101 | template = template or self.extra.get('template', self.template) 102 | return ((template % self.extra), params) #Returns a tuple of (SQL statement without values, list of params to substitute in) 103 | 104 | def as_mysql(self, compiler, connection): 105 | """ 106 | MySQL (and derivatives e.g. MariaDB) have a range of functions for the various intervals plus one standard function 107 | for the lot: TIMESTAMPDIFF(interval, date2, date1). The key thing to note is that the dates are in the inverse order 108 | compared to DATEDIFF... yes I've got no idea why either! 109 | 110 | Years: TIMESTAMPDIFF(YEAR,datestart,dateend) 111 | Months: TIMESTAMPDIFF(MONTH,datestart,dateend) 112 | Days: TIMESTAMPDIFF(DAY,datestart,dateend) 113 | Weeks: TIMESTAMPDIFF(WEEK,datestart,dateend) 114 | Hour: TIMESTAMPDIFF(HOUR,datestart,dateend) 115 | Minute: TIMESTAMPDIFF(MINUTE,datestart,dateend) 116 | Second: TIMESTAMPDIFF(SECOND,datestart,dateend) 117 | """ 118 | interval_expression = "DAY" #Default to no multiplication 119 | if self.interval == "yy": 120 | interval_expression = "YEAR" 121 | elif self.interval == "mm": 122 | interval_expression = "MONTH" 123 | elif self.interval == "dd": 124 | interval_expression = "DAY" 125 | elif self.interval == "wk": 126 | interval_expression = "WEEK" 127 | elif self.interval == "hh": 128 | interval_expression = "HOUR" 129 | elif self.interval == "mi": 130 | interval_expression = "MINUTE" 131 | elif self.interval == "ss": 132 | interval_expression = "SECOND" 133 | # 134 | self.template = 'TRUNCATE(TIMESTAMPDIFF(%(interval_expression)s,%(date_start)s,%(date_end)s),0)' #The ,0 at the end tells TRUNCATE to round to integers 135 | self.extra["interval_expression"] = interval_expression 136 | self.param_order = ("date_start", "date_end") 137 | #Now we can call the standard as_sql to build the statement with these new values 138 | return self.as_sql(compiler, connection) 139 | 140 | def as_postgresql(self, compiler, connection): 141 | """ 142 | PostgreSQL doesn't provide Datediff. Instead it can subtract dates directly, with the ability to return the difference 143 | in any denominator you ask for (days, months, years etc) 144 | 145 | Years: DATE_PART('year', end) - DATE_PART('year', start) 146 | Months: years_diff * 12 + (DATE_PART('month', end) - DATE_PART('month', start)) 147 | Days: DATE_PART('day', end - start) 148 | Weeks: TRUNC(DATE_PART('day', end - start)/7) 149 | Hours: days_diff * 24 + DATE_PART('hour', end - start ) 150 | Minutes: hours_diff * 60 + DATE_PART('minute', end - start ) 151 | Seconds: minutes_diff * 60 + DATE_PART('minute', end - start ) 152 | 153 | @todo: Cast values which are very obviously string dates into PostgreSQL date syntax: e.g. ('2016-05-04'::date) 154 | """ 155 | if self.interval == "yy": 156 | template = "DATE_PART('year', %(date_end)s) - DATE_PART('year', %(date_start)s)" 157 | elif self.interval == "mm": 158 | template = "years_diff * 12 + (DATE_PART('month', %(date_end)s) - DATE_PART('month', %(date_start)s))" 159 | elif self.interval == "dd": 160 | template = "DATE_PART('day', %(date_end)s - %(date_start)s)" 161 | elif self.interval == "wk": 162 | template = "TRUNC(DATE_PART('day', %(date_end)s - %(date_start)s)/7)" 163 | elif self.interval == "hh": 164 | template = "days_diff * 24 + DATE_PART('hour', %(date_end)s - %(date_start)s)" 165 | elif self.interval == "mi": 166 | template = "hours_diff * 60 + DATE_PART('minute', %(date_end)s - %(date_start)s)" 167 | elif self.interval == "ss": 168 | template = "minutes_diff * 60 + DATE_PART('minute', %(date_end)s - %(date_start)s)" 169 | self.template = template 170 | self.param_order = ("date_end", "date_start") 171 | return self.as_sql(compiler, connection) 172 | 173 | def as_oracle(self, compiler, connection): 174 | """ 175 | Oracle's (eye-wateringly overpriced*) database permits subtraction of two dates directly, so long as they are cast 176 | appropriately: 177 | 178 | Years: TRUNC(MONTHS_BETWEEN(date_end, date_start) / 12) 179 | Months: MONTHS_BETWEEN(date_end, date_start) 180 | Days: TRUNC(date_end - date_start) 181 | Weeks: TRUNC((date_end - date_start)/7) 182 | Hours: TRUNC((date_end - date_start)*24) 183 | Minutes: TRUNC((date_end - date_start)*24*60) 184 | Seconds: TRUNC((date_end - date_start)*24*60*60) 185 | 186 | NB: We use TRUNC as it always rounds towards zero, thus important for negative date periods 187 | 188 | @todo: Cast date string values into native Oracle date types: e.g TO_DATE('2000-01-02', 'YYYY-MM-DD') 189 | 190 | *seriously, why are you using this database?! I recommend switching to PostgreSQL. That'll save yourself some 191 | licence fee money and you won't have to untangle Oracle's multitude of different licenses nightmare. If it's 192 | about "support", then PostgreSQL has plenty of third party companies offering support contracts. 193 | """ 194 | if self.interval == "yy": 195 | template = "TRUNC(MONTHS_BETWEEN(%(date_end)s, %(date_start)s) / 12)" 196 | elif self.interval == "mm": 197 | template = "MONTHS_BETWEEN(%(date_end)s, %(date_start)s)" 198 | elif self.interval == "dd": 199 | template = "TRUNC(%(date_end)s - %(date_start)s)" 200 | elif self.interval == "wk": 201 | template = "TRUNC((%(date_end)s - %(date_start)s)/7)" 202 | elif self.interval == "hh": 203 | template = "TRUNC((%(date_end)s - %(date_start)s)*24)" 204 | elif self.interval == "mi": 205 | template = "TRUNC((%(date_end)s - %(date_start)s)*24*60)" 206 | elif self.interval == "ss": 207 | template = "TRUNC((%(date_end)s - %(date_start)s)*24*60*60)" 208 | self.template = template 209 | self.param_order = ("date_end", "date_start") 210 | return self.as_sql(compiler, connection) 211 | 212 | def as_sqlite(self, compiler, connection): 213 | """ 214 | SQLite doesn't carry a DateDiff equivalent. Instead, it allows subtraction between date fields so 215 | long as they are casted into dates with juliandate() 216 | 217 | Years: (this is an approximation as we don't know exactly WHEN the leap years will fall) 218 | Select Cast ( 219 | (JulianDay(SecondDate) - JulianDay(FirstDate)) / 365.242 220 | ) As Integer 221 | 222 | Months: (this is a crude approximation) 223 | Select Cast ( 224 | (JulianDay(SecondDate) - JulianDay(FirstDate)) / 30 225 | ) As Integer 226 | 227 | Weeks: 228 | Select Cast ( 229 | (JulianDay(SecondDate) - JulianDay(FirstDate)) / 7 230 | ) As Integer 231 | 232 | Days: 233 | Select Cast ( 234 | JulianDay(SecondDate) - JulianDay(FirstDate) 235 | ) As Integer 236 | 237 | Hours: 238 | Select Cast ( 239 | (JulianDay(SecondDate) - JulianDay(FirstDate)) * 24 240 | ) As Integer 241 | 242 | Minutes: 243 | Select Cast ( 244 | (JulianDay(SecondDate) - JulianDay(FirstDate)) * 24 * 60 245 | ) As Integer 246 | 247 | Seconds: 248 | Select Cast ( 249 | (JulianDay(SecondDate) - JulianDay(FirstDate)) * 24 * 60 * 60 250 | ) As Integer 251 | """ 252 | multiplier_expression = "" #Default to no multiplication 253 | if self.interval == "yy": 254 | multiplier_expression = " / 365.242" 255 | elif self.interval == "mm": 256 | multiplier_expression = " / 30" 257 | elif self.interval == "dd": 258 | multiplier_expression = "" 259 | elif self.interval == "wk": 260 | multiplier_expression = " / 7" 261 | elif self.interval == "hh": 262 | multiplier_expression = " * 24" 263 | elif self.interval == "mi": 264 | multiplier_expression = " * 24 * 60" 265 | elif self.interval == "ss": 266 | multiplier_expression = " * 24 * 60" 267 | # 268 | self.template = 'Cast((JulianDay(%(date_end)s) - JulianDay(%(date_start)s))%(multiplier_expression)s) AS INTEGER' #Must use the old modulus substitution syntax to work with Django's ORM methods 269 | self.extra["date_start"] = self.source_expressions[1] #The second argument 270 | self.extra["date_end"] = self.source_expressions[0] #The first argument 271 | self.extra["multiplier_expression"] = multiplier_expression #Adds this extra keyword 272 | self.param_order = ("date_end", "date_start") 273 | #Now we can call the standard as_sql to build the statement with these new values 274 | return self.as_sql(compiler, connection) 275 | 276 | --------------------------------------------------------------------------------