diff --git a/frappe/__init__.py b/frappe/__init__.py index 3ea3fc665b..80f486ea87 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -4,19 +4,16 @@ globals attached to frappe module + some utility functions that should probably be moved """ - from __future__ import unicode_literals from werkzeug.local import Local, release_local -from werkzeug.exceptions import NotFound -from MySQLdb import ProgrammingError as SQLError - -import os, sys, importlib, inspect +import os, importlib, inspect import json -from .exceptions import * - +# public from frappe.__version__ import __version__ +from .exceptions import * +from .utils.jinja import get_jenv, get_template, render_template local = Local() @@ -597,67 +594,6 @@ def get_list(doctype, filters=None, fields=None, or_filters=None, docstatus=None run_query = get_list -def get_jenv(): - if not local.jenv: - from jinja2 import Environment, DebugUndefined - import frappe.utils - - # frappe will be loaded last, so app templates will get precedence - jenv = Environment(loader = get_jloader(), - undefined=DebugUndefined) - set_filters(jenv) - - jenv.globals.update({ - "frappe": sys.modules[__name__], - "frappe.utils": frappe.utils, - "get_visible_columns": \ - frappe.get_attr("frappe.templates.pages.print.get_visible_columns"), - "_": _ - }) - - local.jenv = jenv - - return local.jenv - -def get_jloader(): - if not local.jloader: - from jinja2 import ChoiceLoader, PackageLoader - - apps = get_installed_apps() - apps.remove("frappe") - - local.jloader = ChoiceLoader([PackageLoader(app, ".") \ - for app in apps + ["frappe"]]) - - return local.jloader - -def set_filters(jenv): - from frappe.utils import global_date_format, cint - from frappe.website.utils import get_hex_shade - from markdown2 import markdown - from json import dumps - - jenv.filters["global_date_format"] = global_date_format - jenv.filters["markdown"] = markdown - jenv.filters["json"] = dumps - jenv.filters["get_hex_shade"] = get_hex_shade - jenv.filters["len"] = len - jenv.filters["int"] = cint - - # load jenv_filters from hooks.py - for app in get_all_apps(True): - for jenv_filter in (get_hooks(app_name=app).jenv_filter or []): - filter_name, filter_function = jenv_filter.split(":") - jenv.filters[filter_name] = get_attr(filter_function) - -def get_template(path): - return get_jenv().get_template(path) - -def render_template(template, context): - from jinja2 import Template - template = Template(template) - return template.render(**context) - def get_website_route(doctype, name): return db.get_value("Website Route", {"ref_doctype": doctype, "docname": name}) diff --git a/frappe/core/page/data_import_tool/data_import_tool.py b/frappe/core/page/data_import_tool/data_import_tool.py index 4309213597..6f1d56cc47 100644 --- a/frappe/core/page/data_import_tool/data_import_tool.py +++ b/frappe/core/page/data_import_tool/data_import_tool.py @@ -28,7 +28,7 @@ def get_doctype_options(): return [doctype] + [d.options for d in frappe.get_meta(doctype).get_table_fields()] def import_file_by_path(path, ignore_links=False, overwrite=False, submit=False): - from frappe.utils.datautils import read_csv_content + from frappe.utils.csvutils import read_csv_content from frappe.core.page.data_import_tool.importer import upload print "Importing " + path with open(path, "r") as infile: diff --git a/frappe/core/page/data_import_tool/exporter.py b/frappe/core/page/data_import_tool/exporter.py index ab711e171b..c1ef21e410 100644 --- a/frappe/core/page/data_import_tool/exporter.py +++ b/frappe/core/page/data_import_tool/exporter.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, json, os import frappe.permissions -from frappe.utils.datautils import UnicodeWriter +from frappe.utils.csvutils import UnicodeWriter from frappe.utils import cstr, cint, flt from frappe.core.page.data_import_tool.data_import_tool import data_keys diff --git a/frappe/core/page/data_import_tool/importer.py b/frappe/core/page/data_import_tool/importer.py index d481407e1c..a9b2b7a52d 100644 --- a/frappe/core/page/data_import_tool/importer.py +++ b/frappe/core/page/data_import_tool/importer.py @@ -8,7 +8,7 @@ import frappe.permissions from frappe import _ -from frappe.utils.datautils import getlink +from frappe.utils.csvutils import getlink from frappe.utils.dateutils import parse_date from frappe.utils import cint, cstr, flt @@ -27,7 +27,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, if params.get("ignore_encoding_errors"): ignore_encoding_errors = True - from frappe.utils.datautils import read_csv_content_from_uploaded_file + from frappe.utils.csvutils import read_csv_content_from_uploaded_file def bad_template(): frappe.throw(_("Please do not change the rows above {0}").format(data_keys.data_separator)) diff --git a/frappe/core/page/user_permissions/user_permissions.py b/frappe/core/page/user_permissions/user_permissions.py index 3e327ddce9..ef5a86c2b7 100644 --- a/frappe/core/page/user_permissions/user_permissions.py +++ b/frappe/core/page/user_permissions/user_permissions.py @@ -7,7 +7,7 @@ from frappe import _ import frappe.defaults import frappe.permissions from frappe.core.doctype.user.user import get_system_users -from frappe.utils.datautils import UnicodeWriter, read_csv_content_from_uploaded_file +from frappe.utils.csvutils import UnicodeWriter, read_csv_content_from_uploaded_file from frappe.defaults import clear_default @frappe.whitelist() diff --git a/frappe/exceptions.py b/frappe/exceptions.py index f5d47ae33d..19d77beae2 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -5,6 +5,9 @@ from __future__ import unicode_literals # BEWARE don't put anything in this file except exceptions +from werkzeug.exceptions import NotFound +from MySQLdb import ProgrammingError as SQLError + class ValidationError(Exception): http_status_code = 417 diff --git a/frappe/public/html/print_template.html b/frappe/public/html/print_template.html index aba7ff57dc..eec4b04a2c 100644 --- a/frappe/public/html/print_template.html +++ b/frappe/public/html/print_template.html @@ -7,7 +7,7 @@ {%= title %} - +
diff --git a/frappe/public/js/frappe/misc/tools.js b/frappe/public/js/frappe/misc/tools.js index ec90d92efd..d564493c3b 100644 --- a/frappe/public/js/frappe/misc/tools.js +++ b/frappe/public/js/frappe/misc/tools.js @@ -15,7 +15,7 @@ frappe.tools.downloadify = function(data, roles, me) { var download_from_server = function() { open_url_post("/", { args: { data: data, filename: me.title }, - cmd: "frappe.utils.datautils.send_csv_to_client" + cmd: "frappe.utils.csvutils.send_csv_to_client" }, true); } diff --git a/frappe/templates/pages/print.py b/frappe/templates/pages/print.py index 2638211d8d..592800717f 100644 --- a/frappe/templates/pages/print.py +++ b/frappe/templates/pages/print.py @@ -68,7 +68,7 @@ def get_html(doc, name=None, print_format=None, meta=None, if print_format in ("Standard", standard_format): template = jenv.get_template("templates/print_formats/standard.html") else: - template =jenv.from_string(get_print_format(doc.doctype, + template = jenv.from_string(get_print_format(doc.doctype, print_format)) args = { diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index c0478c2fb6..9ed6d508f0 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -26,7 +26,7 @@ Sr {% for tdf in visible_columns %} - + {{ _(tdf.label) }} {% endfor %} @@ -82,8 +82,9 @@ {% endif %} {%- endmacro %} -{% macro get_width(fieldtype) -%} - {%- if fieldtype in ("Int", "Check", "Float", "Currency") -%}{{ 80 }} +{% macro get_width(df) -%} + {%- if df.print_width -%}{{ (df.print_width|str).replace("px", "") }} + {%- elif df.fieldtype in ("Int", "Check", "Float", "Currency") -%}{{ 80 }} {%- else -%}{{ 150 }}{% endif -%} {%- endmacro %} diff --git a/frappe/tests/test_data_import.py b/frappe/tests/test_data_import.py index 0efb5ff7ab..70ef531e8e 100644 --- a/frappe/tests/test_data_import.py +++ b/frappe/tests/test_data_import.py @@ -4,7 +4,7 @@ import frappe, unittest from frappe.core.page.data_import_tool import exporter from frappe.core.page.data_import_tool import importer -from frappe.utils.datautils import read_csv_content +from frappe.utils.csvutils import read_csv_content class TestDataImport(unittest.TestCase): def test_export(self): diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 7501b8a906..76d65af48d 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -5,10 +5,12 @@ from __future__ import unicode_literals from werkzeug.test import Client -import os, sys, re, urllib, datetime, math -import babel.dates +import os, sys, re, urllib import frappe +# utility functions like cint, int, flt, etc. +from frappe.utils.data import * + no_value_fields = ['Section Break', 'Column Break', 'HTML', 'Table', 'FlexTable', 'Button', 'Image', 'Graph'] default_fields = ['doctype', 'name', 'owner', 'creation', 'modified', 'modified_by', @@ -97,170 +99,6 @@ def get_traceback(): def log(event, details): frappe.logger.info(details) -# datetime functions -def getdate(string_date): - """ - Coverts string date (yyyy-mm-dd) to datetime.date object - """ - if isinstance(string_date, datetime.date): - return string_date - elif isinstance(string_date, datetime.datetime): - return datetime.date() - - if " " in string_date: - string_date = string_date.split(" ")[0] - - return datetime.datetime.strptime(string_date, "%Y-%m-%d").date() - -def add_to_date(date, years=0, months=0, days=0): - """Adds `days` to the given date""" - format = isinstance(date, basestring) - if date: - date = getdate(date) - else: - raise Exception, "Start date required" - - from dateutil.relativedelta import relativedelta - date += relativedelta(years=years, months=months, days=days) - - if format: - return date.strftime("%Y-%m-%d") - else: - return date - -def add_days(date, days): - return add_to_date(date, days=days) - -def add_months(date, months): - return add_to_date(date, months=months) - -def add_years(date, years): - return add_to_date(date, years=years) - -def date_diff(string_ed_date, string_st_date): - return (getdate(string_ed_date) - getdate(string_st_date)).days - -def time_diff(string_ed_date, string_st_date): - return get_datetime(string_ed_date) - get_datetime(string_st_date) - -def time_diff_in_seconds(string_ed_date, string_st_date): - return time_diff(string_ed_date, string_st_date).total_seconds() - -def time_diff_in_hours(string_ed_date, string_st_date): - return round(float(time_diff(string_ed_date, string_st_date).total_seconds()) / 3600, 6) - -def now_datetime(): - return convert_utc_to_user_timezone(datetime.datetime.utcnow()) - -def get_user_time_zone(): - if getattr(frappe.local, "user_time_zone", None) is None: - frappe.local.user_time_zone = frappe.cache().get_value("time_zone") - - if not frappe.local.user_time_zone: - frappe.local.user_time_zone = frappe.db.get_default('time_zone') or 'Asia/Calcutta' - frappe.cache().set_value("time_zone", frappe.local.user_time_zone) - - return frappe.local.user_time_zone - -def convert_utc_to_user_timezone(utc_timestamp): - from pytz import timezone, UnknownTimeZoneError - utcnow = timezone('UTC').localize(utc_timestamp) - try: - return utcnow.astimezone(timezone(get_user_time_zone())) - except UnknownTimeZoneError: - return utcnow - -def now(): - """return current datetime as yyyy-mm-dd hh:mm:ss""" - if getattr(frappe.local, "current_date", None): - return getdate(frappe.local.current_date).strftime("%Y-%m-%d") + " " + \ - now_datetime().strftime('%H:%M:%S.%f') - else: - return now_datetime().strftime('%Y-%m-%d %H:%M:%S.%f') - -def nowdate(): - """return current date as yyyy-mm-dd""" - return now_datetime().strftime('%Y-%m-%d') - -def today(): - return nowdate() - -def nowtime(): - """return current time in hh:mm""" - return now_datetime().strftime('%H:%M:%S.%f') - -def get_first_day(dt, d_years=0, d_months=0): - """ - Returns the first day of the month for the date specified by date object - Also adds `d_years` and `d_months` if specified - """ - dt = getdate(dt) - - # d_years, d_months are "deltas" to apply to dt - overflow_years, month = divmod(dt.month + d_months - 1, 12) - year = dt.year + d_years + overflow_years - - return datetime.date(year, month + 1, 1) - -def get_last_day(dt): - """ - Returns last day of the month using: - `get_first_day(dt, 0, 1) + datetime.timedelta(-1)` - """ - return get_first_day(dt, 0, 1) + datetime.timedelta(-1) - -def get_datetime(datetime_str): - try: - return datetime.datetime.strptime(datetime_str, '%Y-%m-%d %H:%M:%S.%f') - - except TypeError: - if isinstance(datetime_str, datetime.datetime): - return datetime_str.replace(tzinfo=None) - else: - raise - - except ValueError: - if datetime_str=='0000-00-00 00:00:00.000000': - return None - - return datetime.datetime.strptime(datetime_str, '%Y-%m-%d %H:%M:%S') - -def get_datetime_str(datetime_obj): - if isinstance(datetime_obj, basestring): - datetime_obj = get_datetime(datetime_obj) - - return datetime_obj.strftime('%Y-%m-%d %H:%M:%S.%f') - -def formatdate(string_date=None, format_string=None): - """ - Convers the given string date to :data:`user_format` - User format specified in defaults - - Examples: - - * dd-mm-yyyy - * mm-dd-yyyy - * dd/mm/yyyy - """ - date = getdate(string_date) if string_date else now_datetime().date() - - if format_string: - return babel.dates.format_date(date, format_string or "medium", locale=(frappe.local.lang or "").replace("-", "_")) - else: - if getattr(frappe.local, "user_format", None) is None: - frappe.local.user_format = frappe.db.get_default("date_format") - - out = frappe.local.user_format - - return out.replace("dd", date.strftime("%d"))\ - .replace("mm", date.strftime("%m"))\ - .replace("yyyy", date.strftime("%Y")) - -def global_date_format(date): - """returns date as 1 January 2012""" - formatted_date = getdate(date).strftime("%d %B %Y") - return formatted_date.startswith("0") and formatted_date[1:] or formatted_date - def dict_to_str(args, sep='&'): """ Converts a dictionary to URL @@ -270,240 +108,6 @@ def dict_to_str(args, sep='&'): t.append(str(k)+'='+urllib.quote(str(args[k] or ''))) return sep.join(t) -def has_common(l1, l2): - """Returns truthy value if there are common elements in lists l1 and l2""" - return set(l1) & set(l2) - -def flt(s, precision=None): - """Convert to float (ignore commas)""" - if isinstance(s, basestring): - s = s.replace(',','') - try: - num = float(s) - if precision is not None: - num = _round(num, precision) - except Exception: - num = 0 - return num - -def cint(s): - """Convert to integer""" - try: num = int(float(s)) - except: num = 0 - return num - -def cstr(s): - if isinstance(s, unicode): - return s - elif s==None: - return '' - elif isinstance(s, basestring): - return unicode(s, 'utf-8') - else: - return unicode(s) - -def _round(num, precision=0): - """round method for round halfs to nearest even algorithm""" - precision = cint(precision) - multiplier = 10 ** precision - - # avoid rounding errors - num = round(num * multiplier if precision else num, 8) - - floor = math.floor(num) - decimal_part = num - floor - - if decimal_part == 0.5: - num = floor if (floor % 2 == 0) else floor + 1 - else: - num = round(num) - - return (num / multiplier) if precision else num - -def encode(obj, encoding="utf-8"): - if isinstance(obj, list): - out = [] - for o in obj: - if isinstance(o, unicode): - out.append(o.encode(encoding)) - else: - out.append(o) - return out - elif isinstance(obj, unicode): - return obj.encode(encoding) - else: - return obj - -def parse_val(v): - """Converts to simple datatypes from SQL query results""" - if isinstance(v, (datetime.date, datetime.datetime)): - v = unicode(v) - elif isinstance(v, datetime.timedelta): - v = ":".join(unicode(v).split(":")[:2]) - elif isinstance(v, long): - v = int(v) - return v - -def fmt_money(amount, precision=None, currency=None): - """ - Convert to string with commas for thousands, millions etc - """ - number_format = None - if currency: - number_format = frappe.db.get_value("Currency", currency, "number_format") - - if not number_format: - number_format = frappe.db.get_default("number_format") or "#,###.##" - - decimal_str, comma_str, number_format_precision = get_number_format_info(number_format) - - if not precision: - precision = number_format_precision - - amount = '%.*f' % (precision, flt(amount)) - if amount.find('.') == -1: - decimals = '' - else: - decimals = amount.split('.')[1] - - parts = [] - minus = '' - if flt(amount) < 0: - minus = '-' - - amount = cstr(abs(flt(amount))).split('.')[0] - - if len(amount) > 3: - parts.append(amount[-3:]) - amount = amount[:-3] - - val = number_format=="#,##,###.##" and 2 or 3 - - while len(amount) > val: - parts.append(amount[-val:]) - amount = amount[:-val] - - parts.append(amount) - - parts.reverse() - - amount = comma_str.join(parts) + ((precision and decimal_str) and (decimal_str + decimals) or "") - amount = minus + amount - - if currency: - symbol = frappe.db.get_value("Currency", currency, "symbol") - if symbol: - amount = symbol + " " + amount - - return amount - -number_format_info = { - "#,###.##": (".", ",", 2), - "#.###,##": (",", ".", 2), - "# ###.##": (".", " ", 2), - "# ###,##": (",", " ", 2), - "#'###.##": (".", "'", 2), - "#, ###.##": (".", ", ", 2), - "#,##,###.##": (".", ",", 2), - "#,###.###": (".", ",", 3), - "#.###": ("", ".", 0), - "#,###": ("", ",", 0) -} - -def get_number_format_info(format): - return number_format_info.get(format) or (".", ",", 2) - -# -# convet currency to words -# -def money_in_words(number, main_currency = None, fraction_currency=None): - """ - Returns string in words with currency and fraction currency. - """ - - d = get_defaults() - if not main_currency: - main_currency = d.get('currency', 'INR') - if not fraction_currency: - fraction_currency = frappe.db.get_value("Currency", main_currency, "fraction") or "Cent" - - n = "%.2f" % flt(number) - main, fraction = n.split('.') - if len(fraction)==1: fraction += '0' - - - number_format = frappe.db.get_value("Currency", main_currency, "number_format") or \ - frappe.db.get_default("number_format") or "#,###.##" - - in_million = True - if number_format == "#,##,###.##": in_million = False - - out = main_currency + ' ' + in_words(main, in_million).title() - if cint(fraction): - out = out + ' and ' + in_words(fraction, in_million).title() + ' ' + fraction_currency - - return out + ' only.' - -# -# convert number to words -# -def in_words(integer, in_million=True): - """ - Returns string in words for the given integer. - """ - n=int(integer) - known = {0: 'zero', 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six', 7: 'seven', 8: 'eight', 9: 'nine', 10: 'ten', - 11: 'eleven', 12: 'twelve', 13: 'thirteen', 14: 'fourteen', 15: 'fifteen', 16: 'sixteen', 17: 'seventeen', 18: 'eighteen', - 19: 'nineteen', 20: 'twenty', 30: 'thirty', 40: 'forty', 50: 'fifty', 60: 'sixty', 70: 'seventy', 80: 'eighty', 90: 'ninety'} - - def psn(n, known, xpsn): - import sys; - if n in known: return known[n] - bestguess, remainder = str(n), 0 - - if n<=20: - frappe.errprint(sys.stderr) - frappe.errprint(n) - frappe.errprint("How did this happen?") - assert 0 - elif n < 100: - bestguess= xpsn((n//10)*10, known, xpsn) + '-' + xpsn(n%10, known, xpsn) - return bestguess - elif n < 1000: - bestguess= xpsn(n//100, known, xpsn) + ' ' + 'hundred' - remainder = n%100 - else: - if in_million: - if n < 1000000: - bestguess= xpsn(n//1000, known, xpsn) + ' ' + 'thousand' - remainder = n%1000 - elif n < 1000000000: - bestguess= xpsn(n//1000000, known, xpsn) + ' ' + 'million' - remainder = n%1000000 - else: - bestguess= xpsn(n//1000000000, known, xpsn) + ' ' + 'billion' - remainder = n%1000000000 - else: - if n < 100000: - bestguess= xpsn(n//1000, known, xpsn) + ' ' + 'thousand' - remainder = n%1000 - elif n < 10000000: - bestguess= xpsn(n//100000, known, xpsn) + ' ' + 'lakh' - remainder = n%100000 - else: - bestguess= xpsn(n//10000000, known, xpsn) + ' ' + 'crore' - remainder = n%10000000 - if remainder: - if remainder >= 100: - comma = ',' - else: - comma = '' - return bestguess + comma + ' ' + xpsn(remainder, known, xpsn) - else: - return bestguess - - return psn(n, known, psn) - # Get Defaults # ============================================================================== @@ -639,103 +243,6 @@ def unesc(s, esc_chars): s = s.replace(esc_str, c) return s -def is_html(text): - out = False - for key in ["
", " - """ - return re.compile(r'<.*?>').sub('', text) - -def escape_html(text): - html_escape_table = { - "&": "&", - '"': """, - "'": "'", - ">": ">", - "<": "<", - } - - return "".join(html_escape_table.get(c,c) for c in text) - -def get_doctype_label(dt=None): - """ - Gets label of a doctype - """ - if dt: - res = frappe.db.sql("""\ - SELECT name, dt_label FROM `tabDocType Label` - WHERE name=%s""", dt) - return res and res[0][0] or dt - else: - res = frappe.db.sql("SELECT name, dt_label FROM `tabDocType Label`") - dt_label_dict = {} - for r in res: - dt_label_dict[r[0]] = r[1] - - return dt_label_dict - - -def get_label_doctype(label): - """ - Gets doctype from its label - """ - res = frappe.db.sql("""\ - SELECT name FROM `tabDocType Label` - WHERE dt_label=%s""", label) - - return res and res[0][0] or label - - -def pretty_date(iso_datetime): - """ - Takes an ISO time and returns a string representing how - long ago the date represents. - Ported from PrettyDate by John Resig - """ - if not iso_datetime: return '' - import math - - if isinstance(iso_datetime, basestring): - iso_datetime = datetime.datetime.strptime(iso_datetime, '%Y-%m-%d %H:%M:%S.%f') - now_dt = datetime.datetime.strptime(now(), '%Y-%m-%d %H:%M:%S.%f') - dt_diff = now_dt - iso_datetime - - # available only in python 2.7+ - # dt_diff_seconds = dt_diff.total_seconds() - - dt_diff_seconds = dt_diff.days * 86400.0 + dt_diff.seconds - - dt_diff_days = math.floor(dt_diff_seconds / 86400.0) - - # differnt cases - if dt_diff_seconds < 60.0: - return 'just now' - elif dt_diff_seconds < 120.0: - return '1 minute ago' - elif dt_diff_seconds < 3600.0: - return '%s minutes ago' % cint(math.floor(dt_diff_seconds / 60.0)) - elif dt_diff_seconds < 7200.0: - return '1 hour ago' - elif dt_diff_seconds < 86400.0: - return '%s hours ago' % cint(math.floor(dt_diff_seconds / 3600.0)) - elif dt_diff_days == 1.0: - return 'Yesterday' - elif dt_diff_days < 7.0: - return '%s days ago' % cint(dt_diff_days) - elif dt_diff_days < 31.0: - return '%s week(s) ago' % cint(math.ceil(dt_diff_days / 7.0)) - elif dt_diff_days < 365.0: - return '%s months ago' % cint(math.ceil(dt_diff_days / 30.0)) - else: - return 'more than %s year(s) ago' % cint(math.floor(dt_diff_days / 365.0)) - def execute_in_shell(cmd, verbose=0): # using Popen instead of os.system - as recommended by python docs from subprocess import Popen @@ -758,30 +265,6 @@ def execute_in_shell(cmd, verbose=0): return err, out -def comma_or(some_list): - return comma_sep(some_list, " or ") - -def comma_and(some_list): - return comma_sep(some_list, " and ") - -def comma_sep(some_list, sep): - if isinstance(some_list, (list, tuple)): - # list(some_list) is done to preserve the existing list - some_list = [unicode(s) for s in list(some_list)] - if not some_list: - return "" - elif len(some_list) == 1: - return some_list[0] - else: - some_list = ["'%s'" % s for s in some_list] - return ", ".join(some_list[:-1]) + sep + some_list[-1] - else: - return some_list - -def filter_strip_join(some_list, sep): - """given a list, filter None values, strip spaces and join""" - return (cstr(sep)).join((cstr(a).strip() for a in filter(None, some_list))) - def get_path(*path, **kwargs): base = kwargs.get('base') if not base: @@ -803,37 +286,6 @@ def get_backups_path(): def get_request_site_address(full_address=False): return get_url(full_address=full_address) -def get_url(uri=None, full_address=False): - """get app url from request""" - host_name = frappe.local.conf.host_name - - if not host_name: - if hasattr(frappe.local, "request") and frappe.local.request and frappe.local.request.host: - protocol = 'https' == frappe.get_request_header('X-Forwarded-Proto', "") and 'https://' or 'http://' - host_name = protocol + frappe.local.request.host - elif frappe.local.site: - host_name = "http://{}".format(frappe.local.site) - else: - host_name = frappe.db.get_value("Website Settings", "Website Settings", - "subdomain") - if host_name and "http" not in host_name: - host_name = "http://" + host_name - - if not host_name: - host_name = "http://localhost" - - if not uri and full_address: - uri = frappe.get_request_header("REQUEST_URI", "") - - url = urllib.basejoin(host_name, uri) if uri else host_name - - return url - -def get_url_to_form(doctype, name, label=None): - if not label: label = name - - return """%(label)s""" % locals() - def encode_dict(d, encoding="utf-8"): for key in d: if isinstance(d[key], basestring) and isinstance(d[key], unicode): @@ -848,34 +300,6 @@ def decode_dict(d, encoding="utf-8"): return d - -import operator -operator_map = { - # startswith - "^": lambda (a, b): (a or "").startswith(b), - - # in or not in a list - "in": lambda (a, b): operator.contains(b, a), - "not in": lambda (a, b): not operator.contains(b, a), - - # comparison operators - "=": lambda (a, b): operator.eq(a, b), - "!=": lambda (a, b): operator.ne(a, b), - ">": lambda (a, b): operator.gt(a, b), - "<": lambda (a, b): operator.lt(a, b), - ">=": lambda (a, b): operator.ge(a, b), - "<=": lambda (a, b): operator.le(a, b), - "not None": lambda (a, b): a and True or False, - "None": lambda (a, b): (not a) and True or False -} - -def compare(val1, condition, val2): - ret = False - if condition in operator_map: - ret = operator_map[condition]((val1, val2)) - - return ret - def get_site_name(hostname): return hostname.split(':')[0] @@ -887,33 +311,6 @@ def get_disk_usage(): err, out = execute_in_shell("du -hsm {files_path}".format(files_path=files_path)) return cint(out.split("\n")[-2].split("\t")[0]) -def scrub_urls(html): - html = expand_relative_urls(html) - html = quote_urls(html) - return html - -def expand_relative_urls(html): - # expand relative urls - url = get_url() - if url.endswith("/"): url = url[:-1] - - def _expand_relative_urls(match): - to_expand = list(match.groups()) - if not to_expand[2].startswith("/"): - to_expand[2] = "/" + to_expand[2] - to_expand.insert(2, url) - return "".join(to_expand) - - return re.sub('(href|src){1}([\s]*=[\s]*[\'"]?)((?!http)[^\'" >]+)([\'"]?)', _expand_relative_urls, html) - -def quote_urls(html): - def _quote_url(match): - groups = list(match.groups()) - groups[2] = urllib.quote(groups[2], safe="~@#$&()*!+=:;,.?/'") - return "".join(groups) - return re.sub('(href|src){1}([\s]*=[\s]*[\'"]?)((?:http)[^\'">]+)([\'"]?)', - _quote_url, html) - def touch_file(path): with open(path, 'a'): os.utime(path, None) diff --git a/frappe/utils/datautils.py b/frappe/utils/csvutils.py similarity index 100% rename from frappe/utils/datautils.py rename to frappe/utils/csvutils.py diff --git a/frappe/utils/data.py b/frappe/utils/data.py new file mode 100644 index 0000000000..fd8e0fd3e5 --- /dev/null +++ b/frappe/utils/data.py @@ -0,0 +1,586 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals + +# IMPORTANT: only import safe functions as this module will be included in jinja environment +import frappe +import operator +import re, urllib, datetime, math +import babel.dates + +# datetime functions +def getdate(string_date): + """ + Coverts string date (yyyy-mm-dd) to datetime.date object + """ + if isinstance(string_date, datetime.date): + return string_date + elif isinstance(string_date, datetime.datetime): + return datetime.date() + + if " " in string_date: + string_date = string_date.split(" ")[0] + + return datetime.datetime.strptime(string_date, "%Y-%m-%d").date() + +def add_to_date(date, years=0, months=0, days=0): + """Adds `days` to the given date""" + format = isinstance(date, basestring) + if date: + date = getdate(date) + else: + raise Exception, "Start date required" + + from dateutil.relativedelta import relativedelta + date += relativedelta(years=years, months=months, days=days) + + if format: + return date.strftime("%Y-%m-%d") + else: + return date + +def add_days(date, days): + return add_to_date(date, days=days) + +def add_months(date, months): + return add_to_date(date, months=months) + +def add_years(date, years): + return add_to_date(date, years=years) + +def date_diff(string_ed_date, string_st_date): + return (getdate(string_ed_date) - getdate(string_st_date)).days + +def time_diff(string_ed_date, string_st_date): + return get_datetime(string_ed_date) - get_datetime(string_st_date) + +def time_diff_in_seconds(string_ed_date, string_st_date): + return time_diff(string_ed_date, string_st_date).total_seconds() + +def time_diff_in_hours(string_ed_date, string_st_date): + return round(float(time_diff(string_ed_date, string_st_date).total_seconds()) / 3600, 6) + +def now_datetime(): + return convert_utc_to_user_timezone(datetime.datetime.utcnow()) + +def get_user_time_zone(): + if getattr(frappe.local, "user_time_zone", None) is None: + frappe.local.user_time_zone = frappe.cache().get_value("time_zone") + + if not frappe.local.user_time_zone: + frappe.local.user_time_zone = frappe.db.get_default('time_zone') or 'Asia/Calcutta' + frappe.cache().set_value("time_zone", frappe.local.user_time_zone) + + return frappe.local.user_time_zone + +def convert_utc_to_user_timezone(utc_timestamp): + from pytz import timezone, UnknownTimeZoneError + utcnow = timezone('UTC').localize(utc_timestamp) + try: + return utcnow.astimezone(timezone(get_user_time_zone())) + except UnknownTimeZoneError: + return utcnow + +def now(): + """return current datetime as yyyy-mm-dd hh:mm:ss""" + if getattr(frappe.local, "current_date", None): + return getdate(frappe.local.current_date).strftime("%Y-%m-%d") + " " + \ + now_datetime().strftime('%H:%M:%S.%f') + else: + return now_datetime().strftime('%Y-%m-%d %H:%M:%S.%f') + +def nowdate(): + """return current date as yyyy-mm-dd""" + return now_datetime().strftime('%Y-%m-%d') + +def today(): + return nowdate() + +def nowtime(): + """return current time in hh:mm""" + return now_datetime().strftime('%H:%M:%S.%f') + +def get_first_day(dt, d_years=0, d_months=0): + """ + Returns the first day of the month for the date specified by date object + Also adds `d_years` and `d_months` if specified + """ + dt = getdate(dt) + + # d_years, d_months are "deltas" to apply to dt + overflow_years, month = divmod(dt.month + d_months - 1, 12) + year = dt.year + d_years + overflow_years + + return datetime.date(year, month + 1, 1) + +def get_last_day(dt): + """ + Returns last day of the month using: + `get_first_day(dt, 0, 1) + datetime.timedelta(-1)` + """ + return get_first_day(dt, 0, 1) + datetime.timedelta(-1) + +def get_datetime(datetime_str): + try: + return datetime.datetime.strptime(datetime_str, '%Y-%m-%d %H:%M:%S.%f') + + except TypeError: + if isinstance(datetime_str, datetime.datetime): + return datetime_str.replace(tzinfo=None) + else: + raise + + except ValueError: + if datetime_str=='0000-00-00 00:00:00.000000': + return None + + return datetime.datetime.strptime(datetime_str, '%Y-%m-%d %H:%M:%S') + +def get_datetime_str(datetime_obj): + if isinstance(datetime_obj, basestring): + datetime_obj = get_datetime(datetime_obj) + + return datetime_obj.strftime('%Y-%m-%d %H:%M:%S.%f') + +def formatdate(string_date=None, format_string=None): + """ + Convers the given string date to :data:`user_format` + User format specified in defaults + + Examples: + + * dd-mm-yyyy + * mm-dd-yyyy + * dd/mm/yyyy + """ + date = getdate(string_date) if string_date else now_datetime().date() + + if format_string: + return babel.dates.format_date(date, format_string or "medium", locale=(frappe.local.lang or "").replace("-", "_")) + else: + if getattr(frappe.local, "user_format", None) is None: + frappe.local.user_format = frappe.db.get_default("date_format") + + out = frappe.local.user_format + + return out.replace("dd", date.strftime("%d"))\ + .replace("mm", date.strftime("%m"))\ + .replace("yyyy", date.strftime("%Y")) + +def global_date_format(date): + """returns date as 1 January 2012""" + formatted_date = getdate(date).strftime("%d %B %Y") + return formatted_date.startswith("0") and formatted_date[1:] or formatted_date + +def has_common(l1, l2): + """Returns truthy value if there are common elements in lists l1 and l2""" + return set(l1) & set(l2) + +def flt(s, precision=None): + """Convert to float (ignore commas)""" + if isinstance(s, basestring): + s = s.replace(',','') + try: + num = float(s) + if precision is not None: + num = rounded(num, precision) + except Exception: + num = 0 + return num + +def cint(s): + """Convert to integer""" + try: num = int(float(s)) + except: num = 0 + return num + +def cstr(s): + if isinstance(s, unicode): + return s + elif s==None: + return '' + elif isinstance(s, basestring): + return unicode(s, 'utf-8') + else: + return unicode(s) + +def rounded(num, precision=0): + """round method for round halfs to nearest even algorithm""" + precision = cint(precision) + multiplier = 10 ** precision + + # avoid rounding errors + num = round(num * multiplier if precision else num, 8) + + floor = math.floor(num) + decimal_part = num - floor + + if decimal_part == 0.5: + num = floor if (floor % 2 == 0) else floor + 1 + else: + num = round(num) + + return (num / multiplier) if precision else num + +def encode(obj, encoding="utf-8"): + if isinstance(obj, list): + out = [] + for o in obj: + if isinstance(o, unicode): + out.append(o.encode(encoding)) + else: + out.append(o) + return out + elif isinstance(obj, unicode): + return obj.encode(encoding) + else: + return obj + +def parse_val(v): + """Converts to simple datatypes from SQL query results""" + if isinstance(v, (datetime.date, datetime.datetime)): + v = unicode(v) + elif isinstance(v, datetime.timedelta): + v = ":".join(unicode(v).split(":")[:2]) + elif isinstance(v, long): + v = int(v) + return v + +def fmt_money(amount, precision=None, currency=None): + """ + Convert to string with commas for thousands, millions etc + """ + number_format = None + if currency: + number_format = frappe.db.get_value("Currency", currency, "number_format") + + if not number_format: + number_format = frappe.db.get_default("number_format") or "#,###.##" + + decimal_str, comma_str, number_format_precision = get_number_format_info(number_format) + + if not precision: + precision = number_format_precision + + amount = '%.*f' % (precision, flt(amount)) + if amount.find('.') == -1: + decimals = '' + else: + decimals = amount.split('.')[1] + + parts = [] + minus = '' + if flt(amount) < 0: + minus = '-' + + amount = cstr(abs(flt(amount))).split('.')[0] + + if len(amount) > 3: + parts.append(amount[-3:]) + amount = amount[:-3] + + val = number_format=="#,##,###.##" and 2 or 3 + + while len(amount) > val: + parts.append(amount[-val:]) + amount = amount[:-val] + + parts.append(amount) + + parts.reverse() + + amount = comma_str.join(parts) + ((precision and decimal_str) and (decimal_str + decimals) or "") + amount = minus + amount + + if currency: + symbol = frappe.db.get_value("Currency", currency, "symbol") + if symbol: + amount = symbol + " " + amount + + return amount + +number_format_info = { + "#,###.##": (".", ",", 2), + "#.###,##": (",", ".", 2), + "# ###.##": (".", " ", 2), + "# ###,##": (",", " ", 2), + "#'###.##": (".", "'", 2), + "#, ###.##": (".", ", ", 2), + "#,##,###.##": (".", ",", 2), + "#,###.###": (".", ",", 3), + "#.###": ("", ".", 0), + "#,###": ("", ",", 0) +} + +def get_number_format_info(format): + return number_format_info.get(format) or (".", ",", 2) + +# +# convet currency to words +# +def money_in_words(number, main_currency = None, fraction_currency=None): + """ + Returns string in words with currency and fraction currency. + """ + from frappe.utils import get_defaults + + d = get_defaults() + if not main_currency: + main_currency = d.get('currency', 'INR') + if not fraction_currency: + fraction_currency = frappe.db.get_value("Currency", main_currency, "fraction") or "Cent" + + n = "%.2f" % flt(number) + main, fraction = n.split('.') + if len(fraction)==1: fraction += '0' + + + number_format = frappe.db.get_value("Currency", main_currency, "number_format") or \ + frappe.db.get_default("number_format") or "#,###.##" + + in_million = True + if number_format == "#,##,###.##": in_million = False + + out = main_currency + ' ' + in_words(main, in_million).title() + if cint(fraction): + out = out + ' and ' + in_words(fraction, in_million).title() + ' ' + fraction_currency + + return out + ' only.' + +# +# convert number to words +# +def in_words(integer, in_million=True): + """ + Returns string in words for the given integer. + """ + n=int(integer) + known = {0: 'zero', 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six', 7: 'seven', 8: 'eight', 9: 'nine', 10: 'ten', + 11: 'eleven', 12: 'twelve', 13: 'thirteen', 14: 'fourteen', 15: 'fifteen', 16: 'sixteen', 17: 'seventeen', 18: 'eighteen', + 19: 'nineteen', 20: 'twenty', 30: 'thirty', 40: 'forty', 50: 'fifty', 60: 'sixty', 70: 'seventy', 80: 'eighty', 90: 'ninety'} + + def psn(n, known, xpsn): + import sys; + if n in known: return known[n] + bestguess, remainder = str(n), 0 + + if n<=20: + frappe.errprint(sys.stderr) + frappe.errprint(n) + frappe.errprint("How did this happen?") + assert 0 + elif n < 100: + bestguess= xpsn((n//10)*10, known, xpsn) + '-' + xpsn(n%10, known, xpsn) + return bestguess + elif n < 1000: + bestguess= xpsn(n//100, known, xpsn) + ' ' + 'hundred' + remainder = n%100 + else: + if in_million: + if n < 1000000: + bestguess= xpsn(n//1000, known, xpsn) + ' ' + 'thousand' + remainder = n%1000 + elif n < 1000000000: + bestguess= xpsn(n//1000000, known, xpsn) + ' ' + 'million' + remainder = n%1000000 + else: + bestguess= xpsn(n//1000000000, known, xpsn) + ' ' + 'billion' + remainder = n%1000000000 + else: + if n < 100000: + bestguess= xpsn(n//1000, known, xpsn) + ' ' + 'thousand' + remainder = n%1000 + elif n < 10000000: + bestguess= xpsn(n//100000, known, xpsn) + ' ' + 'lakh' + remainder = n%100000 + else: + bestguess= xpsn(n//10000000, known, xpsn) + ' ' + 'crore' + remainder = n%10000000 + if remainder: + if remainder >= 100: + comma = ',' + else: + comma = '' + return bestguess + comma + ' ' + xpsn(remainder, known, xpsn) + else: + return bestguess + + return psn(n, known, psn) + +def is_html(text): + out = False + for key in ["
", " + """ + return re.compile(r'<.*?>').sub('', text) + +def escape_html(text): + html_escape_table = { + "&": "&", + '"': """, + "'": "'", + ">": ">", + "<": "<", + } + + return "".join(html_escape_table.get(c,c) for c in text) + +def pretty_date(iso_datetime): + """ + Takes an ISO time and returns a string representing how + long ago the date represents. + Ported from PrettyDate by John Resig + """ + if not iso_datetime: return '' + import math + + if isinstance(iso_datetime, basestring): + iso_datetime = datetime.datetime.strptime(iso_datetime, '%Y-%m-%d %H:%M:%S.%f') + now_dt = datetime.datetime.strptime(now(), '%Y-%m-%d %H:%M:%S.%f') + dt_diff = now_dt - iso_datetime + + # available only in python 2.7+ + # dt_diff_seconds = dt_diff.total_seconds() + + dt_diff_seconds = dt_diff.days * 86400.0 + dt_diff.seconds + + dt_diff_days = math.floor(dt_diff_seconds / 86400.0) + + # differnt cases + if dt_diff_seconds < 60.0: + return 'just now' + elif dt_diff_seconds < 120.0: + return '1 minute ago' + elif dt_diff_seconds < 3600.0: + return '%s minutes ago' % cint(math.floor(dt_diff_seconds / 60.0)) + elif dt_diff_seconds < 7200.0: + return '1 hour ago' + elif dt_diff_seconds < 86400.0: + return '%s hours ago' % cint(math.floor(dt_diff_seconds / 3600.0)) + elif dt_diff_days == 1.0: + return 'Yesterday' + elif dt_diff_days < 7.0: + return '%s days ago' % cint(dt_diff_days) + elif dt_diff_days < 31.0: + return '%s week(s) ago' % cint(math.ceil(dt_diff_days / 7.0)) + elif dt_diff_days < 365.0: + return '%s months ago' % cint(math.ceil(dt_diff_days / 30.0)) + else: + return 'more than %s year(s) ago' % cint(math.floor(dt_diff_days / 365.0)) + +def comma_or(some_list): + return comma_sep(some_list, " or ") + +def comma_and(some_list): + return comma_sep(some_list, " and ") + +def comma_sep(some_list, sep): + if isinstance(some_list, (list, tuple)): + # list(some_list) is done to preserve the existing list + some_list = [unicode(s) for s in list(some_list)] + if not some_list: + return "" + elif len(some_list) == 1: + return some_list[0] + else: + some_list = ["'%s'" % s for s in some_list] + return ", ".join(some_list[:-1]) + sep + some_list[-1] + else: + return some_list + +def filter_strip_join(some_list, sep): + """given a list, filter None values, strip spaces and join""" + return (cstr(sep)).join((cstr(a).strip() for a in filter(None, some_list))) + +def get_url(uri=None, full_address=False): + """get app url from request""" + host_name = frappe.local.conf.host_name + + if not host_name: + if hasattr(frappe.local, "request") and frappe.local.request and frappe.local.request.host: + protocol = 'https' == frappe.get_request_header('X-Forwarded-Proto', "") and 'https://' or 'http://' + host_name = protocol + frappe.local.request.host + elif frappe.local.site: + host_name = "http://{}".format(frappe.local.site) + else: + host_name = frappe.db.get_value("Website Settings", "Website Settings", + "subdomain") + if host_name and "http" not in host_name: + host_name = "http://" + host_name + + if not host_name: + host_name = "http://localhost" + + if not uri and full_address: + uri = frappe.get_request_header("REQUEST_URI", "") + + url = urllib.basejoin(host_name, uri) if uri else host_name + + return url + +def get_url_to_form(doctype, name, label=None): + if not label: label = name + + return """%(label)s""" % locals() + +operator_map = { + # startswith + "^": lambda (a, b): (a or "").startswith(b), + + # in or not in a list + "in": lambda (a, b): operator.contains(b, a), + "not in": lambda (a, b): not operator.contains(b, a), + + # comparison operators + "=": lambda (a, b): operator.eq(a, b), + "!=": lambda (a, b): operator.ne(a, b), + ">": lambda (a, b): operator.gt(a, b), + "<": lambda (a, b): operator.lt(a, b), + ">=": lambda (a, b): operator.ge(a, b), + "<=": lambda (a, b): operator.le(a, b), + "not None": lambda (a, b): a and True or False, + "None": lambda (a, b): (not a) and True or False +} + +def compare(val1, condition, val2): + ret = False + if condition in operator_map: + ret = operator_map[condition]((val1, val2)) + + return ret + +def scrub_urls(html): + html = expand_relative_urls(html) + html = quote_urls(html) + return html + +def expand_relative_urls(html): + # expand relative urls + url = get_url() + if url.endswith("/"): url = url[:-1] + + def _expand_relative_urls(match): + to_expand = list(match.groups()) + if not to_expand[2].startswith("/"): + to_expand[2] = "/" + to_expand[2] + to_expand.insert(2, url) + return "".join(to_expand) + + return re.sub('(href|src){1}([\s]*=[\s]*[\'"]?)((?!http)[^\'" >]+)([\'"]?)', _expand_relative_urls, html) + +def quote_urls(html): + def _quote_url(match): + groups = list(match.groups()) + groups[2] = urllib.quote(groups[2], safe="~@#$&()*!+=:;,.?/'") + return "".join(groups) + return re.sub('(href|src){1}([\s]*=[\s]*[\'"]?)((?:http)[^\'">]+)([\'"]?)', + _quote_url, html) + diff --git a/frappe/utils/jinja.py b/frappe/utils/jinja.py new file mode 100644 index 0000000000..923e2c904d --- /dev/null +++ b/frappe/utils/jinja.py @@ -0,0 +1,94 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt +from __future__ import unicode_literals +from jinja2 import Template + +def get_jenv(): + import frappe + + if not frappe.local.jenv: + from jinja2 import Environment, DebugUndefined + + # frappe will be loaded last, so app templates will get precedence + jenv = Environment(loader = get_jloader(), + undefined=DebugUndefined) + set_filters(jenv) + + jenv.globals.update(get_allowed_functions_for_jenv()) + + frappe.local.jenv = jenv + + return frappe.local.jenv + +def get_template(path): + return get_jenv().get_template(path) + +def render_template(template, context): + template = Template(template) + return template.render(**context) + +def get_allowed_functions_for_jenv(): + import frappe + import frappe.utils.data + + datautils = {} + for key, obj in frappe.utils.data.__dict__.items(): + if key.startswith("_"): + # ignore + continue + + if hasattr(obj, "__call__"): + # only allow functions + datautils[key] = obj + + return { + # make available limited methods of frappe + "frappe": { + "_": frappe._, + "format_value": frappe.format_value, + "local": frappe.local, + "get_hooks": frappe.get_hooks, + "get_meta": frappe.get_meta, + "get_doc": frappe.get_doc, + "get_list": frappe.get_list, + "utils": datautils, + "get_website_route": frappe.get_website_route + }, + "get_visible_columns": \ + frappe.get_attr("frappe.templates.pages.print.get_visible_columns"), + "_": frappe._ + } + +def get_jloader(): + import frappe + if not frappe.local.jloader: + from jinja2 import ChoiceLoader, PackageLoader + + apps = frappe.get_installed_apps() + apps.remove("frappe") + + frappe.local.jloader = ChoiceLoader([PackageLoader(app, ".") \ + for app in apps + ["frappe"]]) + + return frappe.local.jloader + +def set_filters(jenv): + import frappe + from frappe.utils import global_date_format, cint, cstr + from frappe.website.utils import get_hex_shade + from markdown2 import markdown + from json import dumps + + jenv.filters["global_date_format"] = global_date_format + jenv.filters["markdown"] = markdown + jenv.filters["json"] = dumps + jenv.filters["get_hex_shade"] = get_hex_shade + jenv.filters["len"] = len + jenv.filters["int"] = cint + jenv.filters["str"] = cstr + + # load jenv_filters from hooks.py + for app in frappe.get_all_apps(True): + for jenv_filter in (frappe.get_hooks(app_name=app).jenv_filter or []): + filter_name, filter_function = jenv_filter.split(":") + jenv.filters[filter_name] = frappe.get_attr(filter_function)