diff --git a/frappe/__init__.py b/frappe/__init__.py index eb26790942..7c7c93b25d 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -13,7 +13,8 @@ import os, sys, importlib, inspect, json from .exceptions import * from .utils.jinja import get_jenv, get_template, render_template -__version__ = "7.0.47" +__version__ = '7.2.0' +__title__ = "Frappe Framework" local = Local() @@ -46,7 +47,7 @@ def _(msg, lang=None): lang = local.lang # msg should always be unicode - msg = cstr(msg) + msg = cstr(msg).strip() return get_full_dict(local.lang).get(msg) or msg @@ -55,8 +56,6 @@ def get_lang_dict(fortype, name=None): :param fortype: must be one of `doctype`, `page`, `report`, `include`, `jsfile`, `boot` :param name: name of the document for which assets are to be returned.""" - if local.lang=="en": - return {} from frappe.translate import get_dict return get_dict(fortype, name) @@ -172,7 +171,9 @@ def get_site_config(sites_path=None, site_path=None): if os.path.exists(site_config): config.update(get_file_json(site_config)) elif local.site and not local.flags.new_site: - raise IncorrectSitePath, "{0} does not exist".format(site_config) + print "{0} does not exist".format(local.site) + sys.exit(1) + #raise IncorrectSitePath, "{0} does not exist".format(site_config) return _dict(config) @@ -273,7 +274,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, if as_table and type(msg) in (list, tuple): out.msg = '' + ''.join([''+''.join(['' % c for c in r])+'' for r in msg]) + '
%s
' - if flags.print_messages: + if flags.print_messages and out.msg: print "Message: " + repr(out.msg).encode("utf-8") if title: @@ -488,7 +489,7 @@ def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=Fals return out -def has_website_permission(doctype, ptype="read", doc=None, user=None, verbose=False): +def has_website_permission(doc=None, ptype='read', user=None, verbose=False): """Raises `frappe.PermissionError` if not permitted. :param doctype: DocType for which permission is to be check. @@ -499,11 +500,21 @@ def has_website_permission(doctype, ptype="read", doc=None, user=None, verbose=F if not user: user = session.user - hooks = (get_hooks("has_website_permission") or {}).get(doctype, []) - if hooks: + if doc: if isinstance(doc, basestring): doc = get_doc(doctype, doc) + doctype = doc.doctype + + if doc.flags.ignore_permissions: + return True + + # check permission in controller + if hasattr(doc, 'has_website_permission'): + return doc.has_website_permission(ptype, verbose=verbose) + + hooks = (get_hooks("has_website_permission") or {}).get(doctype, []) + if hooks: for method in hooks: result = call(method, doc=doc, ptype=ptype, user=user, verbose=verbose) # if even a single permission check is Falsy @@ -553,7 +564,7 @@ def new_doc(doctype, parent_doc=None, parentfield=None, as_dict=False): from frappe.model.create_new import get_new_doc return get_new_doc(doctype, parent_doc, parentfield, as_dict=as_dict) -def set_value(doctype, docname, fieldname, value): +def set_value(doctype, docname, fieldname, value=None): """Set document value. Calls `frappe.client.set_value`""" import frappe.client return frappe.client.set_value(doctype, docname, fieldname, value) @@ -1113,7 +1124,7 @@ def as_json(obj, indent=1): return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler) def are_emails_muted(): - return flags.mute_emails or conf.get("mute_emails") or False + return flags.mute_emails or int(conf.get("mute_emails") or 0) or False def get_test_records(doctype): """Returns list of objects from `test_records.json` in the given doctype's folder.""" @@ -1125,13 +1136,21 @@ def get_test_records(doctype): else: return [] -def format_value(value, df, doc=None, currency=None): +def format_value(*args, **kwargs): """Format value with given field properties. :param value: Value to be formatted. - :param df: DocField object with properties `fieldtype`, `options` etc.""" + :param df: (Optional) DocField object with properties `fieldtype`, `options` etc.""" import frappe.utils.formatters - return frappe.utils.formatters.format_value(value, df, doc, currency=currency) + return frappe.utils.formatters.format_value(*args, **kwargs) + +def format(*args, **kwargs): + """Format value with given field properties. + + :param value: Value to be formatted. + :param df: (Optional) DocField object with properties `fieldtype`, `options` etc.""" + import frappe.utils.formatters + return frappe.utils.formatters.format_value(*args, **kwargs) def get_print(doctype=None, name=None, print_format=None, style=None, html=None, as_pdf=False, doc=None): """Get Print Format for given document. @@ -1241,7 +1260,12 @@ log_level = None def logger(module=None, with_more_info=True): '''Returns a python logger that uses StreamHandler''' from frappe.utils.logger import get_logger - return get_logger(module or __name__, with_more_info=with_more_info) + return get_logger(module or 'default', with_more_info=with_more_info) + +def log_error(message=None, title=None): + '''Log error to Error Log''' + get_doc(dict(doctype='Error Log', error=str(message or get_traceback()), + method=title)).insert(ignore_permissions=True) def get_desk_link(doctype, name): return '{2} {1}'.format(doctype, name, _(doctype)) diff --git a/frappe/api.py b/frappe/api.py index 99bc996f55..0092067ffb 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -8,6 +8,9 @@ import frappe.handler import frappe.client from frappe.utils.response import build_response from frappe import _ +from urlparse import urlparse +from urllib import urlencode +from frappe.integration_broker.oauth2 import oauth_server def handle(): """ @@ -32,6 +35,27 @@ def handle(): `/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method """ + + form_dict = frappe.local.form_dict + authorization_header = frappe.get_request_header("Authorization").split(" ") if frappe.get_request_header("Authorization") else None + if authorization_header and authorization_header[0].lower() == "bearer": + token = authorization_header[1] + r = frappe.request + parsed_url = urlparse(r.url) + access_token = { "access_token": token} + uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token) + http_method = r.method + body = r.get_data() + headers = r.headers + + required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(";") + + valid, oauthlib_request = oauth_server.verify_request(uri, http_method, body, headers, required_scopes) + + if valid: + frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) + frappe.local.form_dict = form_dict + parts = frappe.request.path[1:].split("/",3) call = doctype = name = None diff --git a/frappe/app.py b/frappe/app.py index ffc52bd0e7..8092b5ddc2 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -72,7 +72,6 @@ def application(request): raise NotFound except HTTPException, e: - frappe.logger().error('Request Error', exc_info=True) return e except frappe.SessionStopped, e: diff --git a/frappe/auth.py b/frappe/auth.py index 49480f6902..fcaa9da0da 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -150,6 +150,13 @@ class LoginManager: if not resume: frappe.response["full_name"] = self.full_name + # redirect information + redirect_to = frappe.cache().hget('redirect_after_login', self.user) + if redirect_to: + frappe.local.response["redirect_to"] = redirect_to + frappe.cache().hdel('redirect_after_login', self.user) + + frappe.local.cookie_manager.set_cookie("full_name", self.full_name) frappe.local.cookie_manager.set_cookie("user_id", self.user) frappe.local.cookie_manager.set_cookie("user_image", self.info.user_image or "") diff --git a/frappe/boot.py b/frappe/boot.py index 24e611d377..3ea44bc0d5 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -103,16 +103,18 @@ def get_allowed_pages(): return page_info def load_translations(bootinfo): - if frappe.local.lang != 'en': - messages = frappe.get_lang_dict("boot") + messages = frappe.get_lang_dict("boot") - bootinfo["lang"] = frappe.lang + bootinfo["lang"] = frappe.lang - # load translated report names - for name in bootinfo.user.all_reports: - messages[name] = frappe._(name) + # load translated report names + for name in bootinfo.user.all_reports: + messages[name] = frappe._(name) - bootinfo["__messages"] = messages + # only untranslated + messages = {k:v for k, v in messages.iteritems() if k!=v} + + bootinfo["__messages"] = messages def get_fullnames(): """map of user fullnames""" @@ -165,3 +167,9 @@ def load_print(bootinfo, doclist): def load_print_css(bootinfo, print_settings): bootinfo.print_css = frappe.get_attr("frappe.www.print.get_print_style")(print_settings.print_style or "Modern", for_legacy=True) + +def get_unseen_notes(): + return frappe.db.sql('''select name, title, content from tabNote where notify_on_login=1 + and expire_notification_on > %s and %s not in + (select user from `tabNote Seen By` nsb + where nsb.parent=tabNote.name)''', (frappe.utils.now(), frappe.session.user), as_dict=True) \ No newline at end of file diff --git a/frappe/build.py b/frappe/build.py index e300e19850..2b59eb93fc 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -57,14 +57,19 @@ def make_asset_dirs(make_copy=False): # symlink app/public > assets/app for app_name in frappe.get_all_apps(True): pymodule = frappe.get_module(app_name) - source = os.path.join(os.path.abspath(os.path.dirname(pymodule.__file__)), 'public') - target = os.path.join(assets_path, app_name) + app_base_path = os.path.abspath(os.path.dirname(pymodule.__file__)) - if not os.path.exists(target) and os.path.exists(source): - if make_copy: - shutil.copytree(os.path.abspath(source), target) - else: - os.symlink(os.path.abspath(source), target) + symlinks = [] + symlinks.append([os.path.join(app_base_path, 'public'), os.path.join(assets_path, app_name)]) + symlinks.append([os.path.join(app_base_path, 'docs'), os.path.join(assets_path, app_name + '_docs')]) + + for source, target in symlinks: + source = os.path.abspath(source) + if not os.path.exists(target) and os.path.exists(source): + if make_copy: + shutil.copytree(source, target) + else: + os.symlink(source, target) def build(no_compress=False, verbose=False): assets_path = os.path.join(frappe.local.sites_path, "assets") @@ -72,9 +77,6 @@ def build(no_compress=False, verbose=False): for target, sources in get_build_maps().iteritems(): pack(os.path.join(assets_path, target), sources, no_compress, verbose) - shutil.copy(os.path.join(os.path.dirname(os.path.abspath(frappe.__file__)), 'data', 'languages.txt'), frappe.local.sites_path) - # reset_app_html() - def get_build_maps(): """get all build.jsons with absolute paths""" # framework js and css files diff --git a/frappe/change_log/v7/v7_1_0.md b/frappe/change_log/v7/v7_1_0.md new file mode 100644 index 0000000000..7d2187aebb --- /dev/null +++ b/frappe/change_log/v7/v7_1_0.md @@ -0,0 +1,24 @@ +#### Gantt View +- New Gantt view for documents where date range is available + +#### In-App Help +- Search for help from within the app. Click on "Help" + +#### Web Form +- Add grids (child tables) +- Add page breaks (for long forms) +- Add payment gateway +- Add attachments + +#### Auto Email Report +- Email reports automatically on daily / weekly / monthly basis + +#### Other Fixes +- Send a popup to all users on login for a new Note by checking on "Notify users with a popup when they log in" +- Portal Users (Customers, Supplier, Students) can now have roles +- Sidebar in portal view will be rendered as per roles and can be configured from Portal Settings +- Restrict the number of backups to be saved in System Settings +- Scheduler log is now error log and as MyISAM +- A better way to export customzations and Email Alert directly from Customize Form +- Option to send email from Data Import Tool where applicable +- Integration Broker \ No newline at end of file diff --git a/frappe/client.py b/frappe/client.py index 037c78edfa..81bec3787c 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -47,22 +47,32 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False): return frappe.db.get_value(doctype, filters, fieldname, as_dict=as_dict, debug=debug) @frappe.whitelist() -def set_value(doctype, name, fieldname, value): +def set_value(doctype, name, fieldname, value=None): + '''Set a value using get_doc, group of values + + :param doctype: DocType of the document + :param name: name of the document + :param fieldname: fieldname string or JSON / dict with key value pair + :param value: value if fieldname is JSON / dict''' + if fieldname!="idx" and fieldname in frappe.model.default_fields: frappe.throw(_("Cannot edit standard fields")) + if not value: + values = fieldname + if isinstance(fieldname, basestring): + values = json.loads(fieldname) + else: + values = {fieldname: value} + doc = frappe.db.get_value(doctype, name, ["parenttype", "parent"], as_dict=True) if doc and doc.parent and doc.parenttype: doc = frappe.get_doc(doc.parenttype, doc.parent) child = doc.getone({"doctype": doctype, "name": name}) - child.set(fieldname, value) + child.update(values) else: doc = frappe.get_doc(doctype, name) - df = doc.meta.get_field(fieldname) - if df.fieldtype == "Read Only" or df.read_only: - frappe.throw(_("Can not edit Read Only fields")) - else: - doc.set(fieldname, value) + doc.update(values) doc.save() diff --git a/frappe/commands/docs.py b/frappe/commands/docs.py index 92daaffa58..38d4a22bea 100644 --- a/frappe/commands/docs.py +++ b/frappe/commands/docs.py @@ -52,7 +52,7 @@ def build_docs(context, app, docs_version="current", target=None, local=False, w or "docs.py" in source_path): _build_docs_once(site, app, docs_version, target, local, only_content_updated=True) - apps_path = frappe.get_app_path("frappe", "..", "..") + apps_path = frappe.get_app_path(app, "..", "..") start_watch(apps_path, handler=trigger_make) def _build_docs_once(site, app, docs_version, target, local, only_content_updated=False): diff --git a/frappe/commands/site.py b/frappe/commands/site.py index c291780da3..6f538eda8d 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -66,7 +66,9 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N print "*** Scheduler is", scheduler_status, "***" finally: - os.remove(installing) + if os.path.exists(installing): + os.remove(installing) + frappe.destroy() @click.command('restore') @@ -238,6 +240,20 @@ def reload_doc(context, module, doctype, docname): finally: frappe.destroy() +@click.command('reload-doctype') +@click.argument('doctype') +@pass_context +def reload_doctype(context, doctype): + "Reload schema for a DocType" + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + frappe.reload_doctype(doctype, force=context.force) + frappe.db.commit() + finally: + frappe.destroy() + @click.command('use') @click.argument('site') @@ -455,6 +471,7 @@ commands = [ new_site, reinstall, reload_doc, + reload_doctype, remove_from_installed_apps, restore, run_patch, diff --git a/frappe/commands/translate.py b/frappe/commands/translate.py index 0dd2546aed..cbe0132575 100644 --- a/frappe/commands/translate.py +++ b/frappe/commands/translate.py @@ -33,7 +33,7 @@ def new_language(context, lang_code, app): frappe.translate.write_translations_file(app, lang_code) print "File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format(app=app, lang_code=lang_code) - print "You will need to add the language in frappe/data/languages.txt, if you haven't done it already." + print "You will need to add the language in frappe/geo/languages.json, if you haven't done it already." @click.command('get-untranslated') @click.argument('lang') diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 9798458774..7c74e41834 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -171,7 +171,7 @@ def export_json(context, doctype, path, name=None): @click.argument('path') @pass_context def export_csv(context, doctype, path): - "Export data import template for DocType" + "Export data import template with data for DocType" from frappe.core.page.data_import_tool import data_import_tool for site in context.sites: try: @@ -200,6 +200,13 @@ def export_fixtures(context): def import_doc(context, path, force=False): "Import (insert/update) doclist. If the argument is a directory, all files ending with .json are imported" from frappe.core.page.data_import_tool import data_import_tool + + if not os.path.exists(path): + path = os.path.join('..', path) + if not os.path.exists(path): + print 'Invalid path {0}'.format(path) + sys.exit(1) + for site in context.sites: try: frappe.init(site=site) @@ -213,13 +220,21 @@ def import_doc(context, path, force=False): @click.option('--only-insert', default=False, is_flag=True, help='Do not overwrite existing records') @click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it') @click.option('--ignore-encoding-errors', default=False, is_flag=True, help='Ignore encoding errors while coverting to unicode') +@click.option('--no-email', default=True, is_flag=True, help='Send email if applicable') + @pass_context -def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False): +def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True): "Import CSV using data import tool" from frappe.core.page.data_import_tool import importer from frappe.utils.csvutils import read_csv_content site = get_site(context) + if not os.path.exists(path): + path = os.path.join('..', path) + if not os.path.exists(path): + print 'Invalid path {0}'.format(path) + sys.exit(1) + with open(path, 'r') as csvfile: content = read_csv_content(csvfile.read()) @@ -227,7 +242,7 @@ def import_csv(context, path, only_insert=False, submit_after_import=False, igno frappe.connect() try: - importer.upload(content, submit_after_import=submit_after_import, + importer.upload(content, submit_after_import=submit_after_import, no_email=no_email, ignore_encoding_errors=ignore_encoding_errors, overwrite=not only_insert, via_console=True) frappe.db.commit() @@ -284,8 +299,9 @@ def console(context): @click.option('--driver', help="For Travis") @click.option('--module', help="Run tests in a module") @click.option('--profile', is_flag=True, default=False) +@click.option('--junit-xml-output', help="Destination file path for junit xml report") @pass_context -def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None, profile=False): +def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None, profile=False, junit_xml_output=False): "Run tests" import frappe.test_runner from frappe.utils import sel @@ -299,7 +315,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None try: ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests, - force=context.force, profile=profile) + force=context.force, profile=profile, junit_xml_output=junit_xml_output) if len(ret.failures) == 0 and len(ret.errors) == 0: ret = 0 finally: @@ -382,6 +398,47 @@ def get_version(): if hasattr(module, "__version__"): print "{0} {1}".format(m, module.__version__) + + +@click.command('setup-global-help') +@click.option('--mariadb_root_password') +def setup_global_help(mariadb_root_password=None): + '''setup help table in a separate database that will be + shared by the whole bench and set `global_help_setup` as 1 in + common_site_config.json''' + + from frappe.installer import update_site_config + + frappe.local.flags = frappe._dict() + frappe.local.flags.in_setup_help = True + frappe.local.flags.in_install = True + frappe.local.lang = 'en' + frappe.local.conf = frappe.get_site_config(sites_path='.') + + update_site_config('global_help_setup', 1, + site_config_path=os.path.join('.', 'common_site_config.json')) + + if mariadb_root_password: + frappe.local.conf.root_password = mariadb_root_password + + from frappe.utils.help import sync + sync() + +@click.command('setup-help') +@pass_context +def setup_help(context): + '''Setup help table in the current site (called after migrate)''' + from frappe.utils.help import sync + + for site in context.sites: + try: + frappe.init(site) + frappe.connect() + sync() + finally: + frappe.destroy() + + commands = [ build, clear_cache, @@ -406,4 +463,6 @@ commands = [ watch, _bulk_rename, add_to_email_queue, + setup_global_help, + setup_help ] diff --git a/frappe/config/core.py b/frappe/config/core.py index a4a9ac860f..bdf39dfb3f 100644 --- a/frappe/config/core.py +++ b/frappe/config/core.py @@ -43,7 +43,7 @@ def get_data(): "items": [ { "type": "doctype", - "name": "Scheduler Log", + "name": "Error Log", "description": _("Errors in Background Events"), }, { @@ -51,6 +51,11 @@ def get_data(): "name": "Email Queue", "description": _("Background Email Queue"), }, + { + "type": "page", + "label": _("Background Jobs"), + "name": "background_jobs", + }, { "type": "doctype", "name": "Error Snapshot", diff --git a/frappe/config/setup.py b/frappe/config/setup.py index 2034bc839f..07b96ea85f 100644 --- a/frappe/config/setup.py +++ b/frappe/config/setup.py @@ -75,7 +75,7 @@ def get_data(): }, { "type": "doctype", - "name": "Scheduler Log", + "name": "Error Log", "description": _("Log of error on automated events (scheduler).") }, { @@ -156,8 +156,8 @@ def get_data(): }, { "type": "doctype", - "name": "Email Group Member", - "description": _("Email Group Member List"), + "name": "Auto Email Report", + "description": _("Setup Reports to be emailed at regular intervals"), }, ] }, @@ -222,10 +222,19 @@ def get_data(): }, { "type": "doctype", - "name": "Dropbox Backup", - "description": _("Manage cloud backups on Dropbox"), - "hide_count": True - } + "name": "Integration Service", + "description": _("Centralize access to Integrations"), + }, + { + "type": "doctype", + "name": "OAuth Client", + "description": _("Register OAuth Client App"), + }, + { + "type": "doctype", + "name": "OAuth Provider Settings", + "description": _("Settings for OAuth Provider"), + }, ] }, { diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 18991cbf77..21d593b095 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -15,6 +15,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "label_and_type", "fieldtype": "Section Break", "hidden": 0, @@ -39,6 +40,7 @@ "allow_on_submit": 0, "bold": 1, "collapsible": 0, + "columns": 0, "fieldname": "label", "fieldtype": "Data", "hidden": 0, @@ -67,6 +69,7 @@ "allow_on_submit": 0, "bold": 1, "collapsible": 0, + "columns": 0, "default": "Data", "fieldname": "fieldtype", "fieldtype": "Select", @@ -95,6 +98,7 @@ "allow_on_submit": 0, "bold": 1, "collapsible": 0, + "columns": 0, "fieldname": "fieldname", "fieldtype": "Data", "hidden": 0, @@ -121,6 +125,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "reqd", "fieldtype": "Check", "hidden": 0, @@ -149,6 +154,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", "description": "Set non-standard precision for a Float or Currency field", "fieldname": "precision", @@ -176,6 +182,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)", "fieldname": "length", "fieldtype": "Int", @@ -202,6 +209,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "search_index", "fieldtype": "Check", "hidden": 0, @@ -230,6 +238,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "in_list_view", "fieldtype": "Check", "hidden": 0, @@ -256,6 +265,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "bold", "fieldtype": "Check", "hidden": 0, @@ -281,6 +291,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:doc.fieldtype===\"Section Break\"", "fieldname": "collapsible", "fieldtype": "Check", @@ -307,6 +318,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:doc.fieldtype==\"Section Break\"", "fieldname": "collapsible_depends_on", "fieldtype": "Code", @@ -333,6 +345,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "column_break_6", "fieldtype": "Column Break", "hidden": 0, @@ -356,6 +369,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.", "fieldname": "options", "fieldtype": "Text", @@ -383,6 +397,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "default", "fieldtype": "Small Text", "hidden": 0, @@ -409,6 +424,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "permissions", "fieldtype": "Section Break", "hidden": 0, @@ -433,6 +449,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "depends_on", "fieldtype": "Code", "hidden": 0, @@ -459,6 +476,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "hidden", "fieldtype": "Check", "hidden": 0, @@ -487,6 +505,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "read_only", "fieldtype": "Check", "hidden": 0, @@ -513,6 +532,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "unique", "fieldtype": "Check", "hidden": 0, @@ -538,6 +558,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "Do not allow user to change after set the first time", "fieldname": "set_only_once", "fieldtype": "Check", @@ -563,6 +584,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "column_break_13", "fieldtype": "Column Break", "hidden": 0, @@ -586,6 +608,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "0", "fieldname": "permlevel", "fieldtype": "Int", @@ -615,6 +638,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "User permissions should not apply for this Link", "fieldname": "ignore_user_permissions", "fieldtype": "Check", @@ -640,6 +664,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "allow_on_submit", "fieldtype": "Check", "hidden": 0, @@ -668,6 +693,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "report_hide", "fieldtype": "Check", "hidden": 0, @@ -696,6 +722,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", "fieldname": "ignore_xss_filter", "fieldtype": "Check", @@ -722,6 +749,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "display", "fieldtype": "Section Break", "hidden": 0, @@ -746,6 +774,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "in_filter", "fieldtype": "Check", "hidden": 0, @@ -774,6 +803,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "no_copy", "fieldtype": "Check", "hidden": 0, @@ -802,6 +832,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "print_hide", "fieldtype": "Check", "hidden": 0, @@ -830,6 +861,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", "fieldname": "print_hide_if_no_value", "fieldtype": "Check", @@ -856,6 +888,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "print_width", "fieldtype": "Data", "hidden": 0, @@ -880,6 +913,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "width", "fieldtype": "Data", "hidden": 0, @@ -908,6 +942,35 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, + "depends_on": "", + "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", + "fieldname": "columns", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Columns", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, "fieldname": "column_break_22", "fieldtype": "Column Break", "hidden": 0, @@ -931,6 +994,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "description", "fieldtype": "Small Text", "hidden": 0, @@ -959,6 +1023,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "oldfieldname", "fieldtype": "Data", "hidden": 1, @@ -984,6 +1049,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "oldfieldtype", "fieldtype": "Data", "hidden": 1, @@ -1016,7 +1082,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-07-11 03:25:57.882851", + "modified": "2016-08-23 11:59:07.036627", "modified_by": "Administrator", "module": "Core", "name": "DocField", @@ -1026,5 +1092,5 @@ "read_only": 0, "read_only_onload": 0, "sort_order": "ASC", - "track_seen": 0 -} \ No newline at end of file + "track_seen": 0 +} diff --git a/frappe/core/doctype/docperm/docperm.json b/frappe/core/doctype/docperm/docperm.json index 8f11e823b7..be205b2fb7 100644 --- a/frappe/core/doctype/docperm/docperm.json +++ b/frappe/core/doctype/docperm/docperm.json @@ -14,6 +14,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "role_and_level", "fieldtype": "Section Break", "hidden": 0, @@ -38,6 +39,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "role", "fieldtype": "Link", "hidden": 0, @@ -67,6 +69,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "Filter records based on User Permissions defined for a user", "fieldname": "apply_user_permissions", "fieldtype": "Check", @@ -92,6 +95,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "Apply this rule if the User is the Owner", "fieldname": "if_owner", "fieldtype": "Check", @@ -118,6 +122,34 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "is_custom", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Is Custom", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, "fieldname": "column_break_2", "fieldtype": "Column Break", "hidden": 0, @@ -141,6 +173,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "0", "fieldname": "permlevel", "fieldtype": "Int", @@ -170,6 +203,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "", "description": "JSON list of DocTypes used to apply User Permissions. If empty, all linked DocTypes will be used to apply User Permissions.", "fieldname": "user_permission_doctypes", @@ -196,6 +230,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "section_break_4", "fieldtype": "Section Break", "hidden": 0, @@ -220,6 +255,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "1", "fieldname": "read", "fieldtype": "Check", @@ -249,6 +285,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "1", "fieldname": "write", "fieldtype": "Check", @@ -278,6 +315,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "1", "fieldname": "create", "fieldtype": "Check", @@ -307,6 +345,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "1", "fieldname": "delete", "fieldtype": "Check", @@ -332,6 +371,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "column_break_8", "fieldtype": "Column Break", "hidden": 0, @@ -355,6 +395,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "submit", "fieldtype": "Check", "hidden": 0, @@ -383,6 +424,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "cancel", "fieldtype": "Check", "hidden": 0, @@ -411,6 +453,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "amend", "fieldtype": "Check", "hidden": 0, @@ -439,6 +482,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "additional_permissions", "fieldtype": "Section Break", "hidden": 0, @@ -463,6 +507,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "1", "fieldname": "report", "fieldtype": "Check", @@ -490,6 +535,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "1", "fieldname": "export", "fieldtype": "Check", @@ -515,6 +561,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "import", "fieldtype": "Check", "hidden": 0, @@ -539,6 +586,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "This role update User Permissions for a user", "fieldname": "set_user_permissions", "fieldtype": "Check", @@ -564,6 +612,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "column_break_19", "fieldtype": "Column Break", "hidden": 0, @@ -587,6 +636,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "1", "fieldname": "share", "fieldtype": "Check", @@ -613,6 +663,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "1", "fieldname": "print", "fieldtype": "Check", @@ -638,6 +689,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "1", "fieldname": "email", "fieldtype": "Check", @@ -670,7 +722,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-07-11 03:27:59.695670", + "modified": "2016-09-29 08:07:20.450064", "modified_by": "Administrator", "module": "Core", "name": "DocPerm", @@ -679,5 +731,6 @@ "quick_entry": 0, "read_only": 0, "read_only_onload": 0, + "sort_order": "ASC", "track_seen": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/docshare/docshare.py b/frappe/core/doctype/docshare/docshare.py index 047d24a9dd..28304fb636 100644 --- a/frappe/core/doctype/docshare/docshare.py +++ b/frappe/core/doctype/docshare/docshare.py @@ -19,9 +19,7 @@ class DocShare(Document): self.get_doc().run_method("validate_share", self) def cascade_permissions_downwards(self): - if self.share: - self.write = 1 - if self.write: + if self.share or self.write: self.read = 1 def get_doc(self): diff --git a/frappe/core/doctype/docshare/test_docshare.py b/frappe/core/doctype/docshare/test_docshare.py index e4a41ac0d2..10986d9eae 100644 --- a/frappe/core/doctype/docshare/test_docshare.py +++ b/frappe/core/doctype/docshare/test_docshare.py @@ -34,7 +34,7 @@ class TestDocShare(unittest.TestCase): self.assertTrue(self.event.has_permission()) def test_share_permission(self): - frappe.share.add("Event", self.event.name, self.user, share=1) + frappe.share.add("Event", self.event.name, self.user, write=1, share=1) frappe.set_user(self.user) self.assertTrue(self.event.has_permission("share")) @@ -60,14 +60,14 @@ class TestDocShare(unittest.TestCase): self.assertRaises(frappe.PermissionError, frappe.share.add, "Event", self.event.name, self.user) frappe.set_user("Administrator") - frappe.share.add("Event", self.event.name, self.user, share=1) + frappe.share.add("Event", self.event.name, self.user, write=1, share=1) # test not raises frappe.set_user(self.user) - frappe.share.add("Event", self.event.name, "test1@example.com", share=1) + frappe.share.add("Event", self.event.name, "test1@example.com", write=1, share=1) def test_remove_share(self): - frappe.share.add("Event", self.event.name, self.user, share=1) + frappe.share.add("Event", self.event.name, self.user, write=1, share=1) frappe.set_user(self.user) self.assertTrue(self.event.has_permission("share")) diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index c4019ec8f8..fc9c594006 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -11,18 +11,25 @@ // } // }) -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - if(doc.__islocal && (user !== "Administrator" || !frappe.boot.developer_mode)) { - cur_frm.set_value("custom", 1); - cur_frm.toggle_enable("custom", 0); - } +frappe.ui.form.on('DocType', { + refresh: function(frm) { + if(frm.doc.__islocal && (user !== "Administrator" || !frappe.boot.developer_mode)) { + frm.set_value("custom", 1); + frm.toggle_enable("custom", 0); + } + + if(!frappe.boot.developer_mode && !frm.doc.custom) { + // make the document read-only + frm.set_read_only(); + } - if(!frappe.boot.developer_mode && !doc.custom) { - // make the document read-only - cur_frm.set_read_only(); + if(!frm.doc.__islocal) { + frm.toggle_enable("engine", 0); + } } -} +}) +// for legacy... :) cur_frm.cscript.validate = function(doc, cdt, cdn) { doc.server_code_compiled = null; } diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 2974bb3159..f7eb5b2180 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -11,11 +11,13 @@ "doctype": "DocType", "document_type": "Document", "editable_grid": 0, + "engine": "InnoDB", "fields": [ { "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "sb0", "fieldtype": "Section Break", "hidden": 0, @@ -41,6 +43,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "module", "fieldtype": "Link", "hidden": 0, @@ -68,6 +71,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "Child Tables are shown as a Grid in other DocTypes.", "fieldname": "istable", "fieldtype": "Check", @@ -95,6 +99,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "1", "depends_on": "istable", "fieldname": "editable_grid", @@ -122,6 +127,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "Single Types have only one record no tables associated. Values are stored in tabSingles", "fieldname": "issingle", "fieldtype": "Check", @@ -149,6 +155,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "cb01", "fieldtype": "Column Break", "hidden": 0, @@ -172,6 +179,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "document_type", "fieldtype": "Select", "hidden": 0, @@ -199,6 +207,36 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, + "default": "InnoDB", + "depends_on": "eval:!doc.issingle", + "fieldname": "engine", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Database Engine", + "length": 0, + "no_copy": 0, + "options": "InnoDB\nMyISAM", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, "fieldname": "icon", "fieldtype": "Data", "hidden": 0, @@ -223,6 +261,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "custom", "fieldtype": "Check", "hidden": 0, @@ -247,6 +286,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "beta", "fieldtype": "Check", "hidden": 0, @@ -272,6 +312,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "0", "depends_on": "eval: doc.image_field", "fieldname": "image_view", @@ -299,6 +340,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "app", "fieldtype": "Data", "hidden": 1, @@ -323,6 +365,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "fields_section_break", "fieldtype": "Section Break", "hidden": 0, @@ -348,6 +391,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "fields", "fieldtype": "Table", "hidden": 0, @@ -375,6 +419,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "sb1", "fieldtype": "Section Break", "hidden": 0, @@ -399,6 +444,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "Naming Options:\n
  1. field:[fieldname] - By Field
  2. naming_series: - By Naming Series (field called naming_series must be present
  3. Prompt - Prompt user for a name
  4. [series] - Series by prefix (separated by a dot); for example PRE.#####
", "fieldname": "autoname", "fieldtype": "Data", @@ -426,6 +472,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "name_case", "fieldtype": "Select", "hidden": 0, @@ -453,6 +500,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "description", "fieldtype": "Small Text", "hidden": 0, @@ -479,6 +527,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "", "fieldname": "column_break_15", "fieldtype": "Column Break", @@ -503,6 +552,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:!doc.istable", "description": "Show this field as title", "fieldname": "title_field", @@ -529,6 +579,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:!doc.istable", "fieldname": "search_fields", "fieldtype": "Data", @@ -556,6 +607,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "Must be of type \"Attach Image\"", "fieldname": "image_field", "fieldtype": "Data", @@ -582,6 +634,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "modified", "depends_on": "eval:!doc.istable", "description": "", @@ -609,6 +662,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "DESC", "depends_on": "eval:!doc.istable", "fieldname": "sort_order", @@ -636,6 +690,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:!doc.istable", "description": "Comments and Communications will be associated with this linked document", "fieldname": "timeline_field", @@ -663,6 +718,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:!doc.istable", "fieldname": "sb2", "fieldtype": "Section Break", @@ -688,6 +744,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "", "fieldname": "permissions", "fieldtype": "Table", @@ -716,6 +773,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:!doc.istable", "fieldname": "sb3", "fieldtype": "Section Break", @@ -740,6 +798,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "cb30", "fieldtype": "Column Break", "hidden": 0, @@ -764,6 +823,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "in_create", "fieldtype": "Check", "hidden": 0, @@ -790,6 +850,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "read_only", "fieldtype": "Check", "hidden": 0, @@ -816,6 +877,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "is_submittable", "fieldtype": "Check", "hidden": 0, @@ -840,6 +902,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "Allow Import via Data Import Tool", "fieldname": "allow_import", "fieldtype": "Check", @@ -865,6 +928,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "allow_rename", "fieldtype": "Check", "hidden": 0, @@ -891,6 +955,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "in_dialog", "fieldtype": "Check", "hidden": 0, @@ -917,6 +982,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "read_only_onload", "fieldtype": "Check", "hidden": 0, @@ -943,6 +1009,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "max_attachments", "fieldtype": "Int", "hidden": 0, @@ -969,6 +1036,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "cb31", "fieldtype": "Column Break", "hidden": 0, @@ -993,6 +1061,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "hide_heading", "fieldtype": "Check", "hidden": 0, @@ -1019,6 +1088,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "hide_toolbar", "fieldtype": "Check", "hidden": 0, @@ -1045,6 +1115,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "allow_copy", "fieldtype": "Check", "hidden": 0, @@ -1071,6 +1142,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "track_seen", "fieldtype": "Check", "hidden": 0, @@ -1096,6 +1168,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "1", "fieldname": "quick_entry", "fieldtype": "Check", @@ -1122,6 +1195,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "default_print_format", "fieldtype": "Data", "hidden": 0, @@ -1154,7 +1228,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-07-25 12:18:27.724194", + "modified": "2016-10-13 01:13:58.133080", "modified_by": "Administrator", "module": "Core", "name": "DocType", @@ -1170,6 +1244,7 @@ "export": 0, "if_owner": 0, "import": 0, + "is_custom": 0, "permlevel": 0, "print": 1, "read": 1, @@ -1190,6 +1265,7 @@ "export": 0, "if_owner": 0, "import": 0, + "is_custom": 0, "permlevel": 0, "print": 1, "read": 1, diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index cccd603fcf..9a3a27ed35 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -46,8 +46,8 @@ class DocType(Document): elif self.istable: self.allow_import = 0 - self.validate_series() self.scrub_field_names() + self.validate_series() self.validate_document_type() validate_fields(self) @@ -120,6 +120,12 @@ class DocType(Document): if not autoname and self.get("fields", {"fieldname":"naming_series"}): self.autoname = "naming_series:" + # validate field name if autoname field:fieldname is used + if autoname and autoname.startswith('field:'): + field = autoname.split(":")[1] + if not field or field not in [ df.fieldname for df in self.fields ]: + frappe.throw(_("Invalid fieldname '{0}' in autoname".format(field))) + if autoname and (not autoname.startswith('field:')) \ and (not autoname.startswith('eval:')) \ and (not autoname.lower() in ('prompt', 'hash')) \ @@ -593,8 +599,7 @@ def make_module_and_roles(doc, perm_fieldname="permissions"): for role in list(set(roles)): if not frappe.db.exists("Role", role): - r = frappe.get_doc({"doctype": "Role", "role_name": role}) - r.role_name = role + r = frappe.get_doc(dict(doctype= "Role", role_name=role, desk_access=1)) r.flags.ignore_mandatory = r.flags.ignore_permissions = True r.insert() except frappe.DoesNotExistError, e: diff --git a/frappe/docs/user/fr/guides/__init__.py b/frappe/core/doctype/error_log/__init__.py similarity index 100% rename from frappe/docs/user/fr/guides/__init__.py rename to frappe/core/doctype/error_log/__init__.py diff --git a/frappe/core/doctype/error_log/error_log.js b/frappe/core/doctype/error_log/error_log.js new file mode 100644 index 0000000000..4fe8fde5d6 --- /dev/null +++ b/frappe/core/doctype/error_log/error_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Error Log', { + refresh: function(frm) { + + } +}); diff --git a/frappe/core/doctype/scheduler_log/scheduler_log.json b/frappe/core/doctype/error_log/error_log.json similarity index 91% rename from frappe/core/doctype/scheduler_log/scheduler_log.json rename to frappe/core/doctype/error_log/error_log.json index 4412a78a8a..b7e9e1808a 100644 --- a/frappe/core/doctype/scheduler_log/scheduler_log.json +++ b/frappe/core/doctype/error_log/error_log.json @@ -2,7 +2,7 @@ "allow_copy": 0, "allow_import": 0, "allow_rename": 0, - "autoname": "SCHLOG.#####", + "autoname": "Error-.#####", "beta": 0, "creation": "2013-01-16 13:09:40", "custom": 0, @@ -10,11 +10,14 @@ "docstatus": 0, "doctype": "DocType", "document_type": "System", + "editable_grid": 0, + "engine": "MyISAM", "fields": [ { "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "0", "fieldname": "seen", "fieldtype": "Check", @@ -41,6 +44,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "method", "fieldtype": "Data", "hidden": 0, @@ -48,7 +52,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 1, - "label": "Method", + "label": "Title", "length": 0, "no_copy": 0, "permlevel": 0, @@ -65,6 +69,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "error", "fieldtype": "Code", "hidden": 0, @@ -97,10 +102,10 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-07-03 14:24:13.581374", + "modified": "2016-10-06 03:29:47.810715", "modified_by": "Administrator", "module": "Core", - "name": "Scheduler Log", + "name": "Error Log", "owner": "Administrator", "permissions": [ { @@ -113,6 +118,7 @@ "export": 0, "if_owner": 0, "import": 0, + "is_custom": 0, "permlevel": 0, "print": 1, "read": 1, diff --git a/frappe/core/doctype/error_log/error_log.py b/frappe/core/doctype/error_log/error_log.py new file mode 100644 index 0000000000..60e8a5f4df --- /dev/null +++ b/frappe/core/doctype/error_log/error_log.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class ErrorLog(Document): + def onload(self): + if not self.seen: + self.db_set('seen', 1) + frappe.db.commit() + +def set_old_logs_as_seen(): + # set logs as seen + frappe.db.sql("""update `tabError Log` set seen=1 + where seen=0 and datediff(curdate(), creation) > 7""") + + # clear old logs + frappe.db.sql("""delete from `tabError Log` where datediff(curdate(), creation) > 30""") diff --git a/frappe/core/doctype/scheduler_log/scheduler_log_list.js b/frappe/core/doctype/error_log/error_log_list.js similarity index 84% rename from frappe/core/doctype/scheduler_log/scheduler_log_list.js rename to frappe/core/doctype/error_log/error_log_list.js index bb37614d78..a50bfe31f3 100644 --- a/frappe/core/doctype/scheduler_log/scheduler_log_list.js +++ b/frappe/core/doctype/error_log/error_log_list.js @@ -1,4 +1,4 @@ -frappe.listview_settings['Scheduler Log'] = { +frappe.listview_settings['Error Log'] = { add_fields: ["seen"], get_indicator: function(doc) { if(cint(doc.seen)) { diff --git a/frappe/core/doctype/error_log/test_error_log.py b/frappe/core/doctype/error_log/test_error_log.py new file mode 100644 index 0000000000..d93fe07c61 --- /dev/null +++ b/frappe/core/doctype/error_log/test_error_log.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +# test_records = frappe.get_test_records('Error Log') + +class TestErrorLog(unittest.TestCase): + pass diff --git a/frappe/public/html/error_object.html b/frappe/core/doctype/error_snapshot/error_object.html similarity index 100% rename from frappe/public/html/error_object.html rename to frappe/core/doctype/error_snapshot/error_object.html diff --git a/frappe/public/html/error_snapshot.html b/frappe/core/doctype/error_snapshot/error_snapshot.html similarity index 100% rename from frappe/public/html/error_snapshot.html rename to frappe/core/doctype/error_snapshot/error_snapshot.html diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json index 7ad3426de3..add5bdb24c 100644 --- a/frappe/core/doctype/file/file.json +++ b/frappe/core/doctype/file/file.json @@ -3,15 +3,18 @@ "allow_import": 1, "allow_rename": 0, "autoname": "", + "beta": 0, "creation": "2012-12-12 11:19:22", "custom": 0, "docstatus": 0, "doctype": "DocType", + "editable_grid": 0, "fields": [ { "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "file_name", "fieldtype": "Data", "hidden": 0, @@ -38,6 +41,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:!doc.is_folder", "fieldname": "is_private", "fieldtype": "Check", @@ -53,17 +57,18 @@ "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, - "read_only": 1, + "read_only": 0, "report_hide": 0, "reqd": 0, "search_index": 0, - "set_only_once": 1, + "set_only_once": 0, "unique": 0 }, { "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "preview", "fieldtype": "Section Break", "hidden": 0, @@ -89,6 +94,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "preview_html", "fieldtype": "HTML", "hidden": 0, @@ -114,6 +120,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "section_break_5", "fieldtype": "Section Break", "hidden": 0, @@ -138,6 +145,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "", "fieldname": "is_home_folder", "fieldtype": "Check", @@ -164,6 +172,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "is_attachments_folder", "fieldtype": "Check", "hidden": 1, @@ -189,6 +198,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "file_size", "fieldtype": "Int", "hidden": 0, @@ -213,6 +223,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "column_break_5", "fieldtype": "Column Break", "hidden": 0, @@ -237,6 +248,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:!doc.is_folder", "fieldname": "file_url", "fieldtype": "Code", @@ -262,6 +274,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "thumbnail_url", "fieldtype": "Small Text", "hidden": 0, @@ -287,6 +300,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "folder", "fieldtype": "Link", "hidden": 1, @@ -313,6 +327,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "is_folder", "fieldtype": "Check", "hidden": 0, @@ -338,6 +353,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:!doc.is_folder", "fieldname": "section_break_8", "fieldtype": "Section Break", @@ -363,6 +379,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "attached_to_doctype", "fieldtype": "Link", "hidden": 0, @@ -388,6 +405,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "column_break_10", "fieldtype": "Column Break", "hidden": 0, @@ -412,6 +430,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "attached_to_name", "fieldtype": "Data", "hidden": 0, @@ -436,6 +455,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "content_hash", "fieldtype": "Data", "hidden": 1, @@ -460,6 +480,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "lft", "fieldtype": "Int", "hidden": 1, @@ -485,6 +506,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "rgt", "fieldtype": "Int", "hidden": 1, @@ -510,6 +532,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "old_parent", "fieldtype": "Data", "hidden": 1, @@ -536,6 +559,7 @@ "hide_toolbar": 0, "icon": "icon-file", "idx": 1, + "image_view": 0, "in_create": 0, "in_dialog": 0, "is_submittable": 0, @@ -543,7 +567,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2016-02-22 09:23:59.892258", + "modified": "2016-09-21 12:23:34.017457", "modified_by": "Administrator", "module": "Core", "name": "File", @@ -590,7 +614,10 @@ "write": 1 } ], + "quick_entry": 0, "read_only": 0, "read_only_onload": 0, - "title_field": "file_name" + "sort_order": "ASC", + "title_field": "file_name", + "track_seen": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 9dc75f06d9..5849066be2 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -12,6 +12,7 @@ import frappe import json import urllib import os +import shutil import requests import requests.exceptions import StringIO @@ -72,6 +73,23 @@ class File(NestedSet): self.set_folder_size() + if frappe.db.exists('File', {'name': self.name, 'is_folder': 0}): + if not self.is_folder and (self.is_private != self.db_get('is_private')): + private_files = frappe.get_site_path('private', 'files') + public_files = frappe.get_site_path('public', 'files') + + if not self.is_private: + shutil.move(os.path.join(private_files, self.file_name), + os.path.join(public_files, self.file_name)) + + self.file_url = "/files/{0}".format(self.file_name) + + else: + shutil.move(os.path.join(public_files, self.file_name), + os.path.join(private_files, self.file_name)) + + self.file_url = "/private/files/{0}".format(self.file_name) + def set_folder_size(self): """Set folder size if folder""" if self.is_folder and not self.is_new(): @@ -350,4 +368,3 @@ def check_file_permission(file_url): return True raise frappe.PermissionError - diff --git a/frappe/docs/user/fr/guides/app-development/__init__.py b/frappe/core/doctype/language/__init__.py similarity index 100% rename from frappe/docs/user/fr/guides/app-development/__init__.py rename to frappe/core/doctype/language/__init__.py diff --git a/frappe/core/doctype/language/language.js b/frappe/core/doctype/language/language.js new file mode 100644 index 0000000000..e60282ebbf --- /dev/null +++ b/frappe/core/doctype/language/language.js @@ -0,0 +1,8 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Language', { + refresh: function(frm) { + + } +}); diff --git a/frappe/core/doctype/language/language.json b/frappe/core/doctype/language/language.json new file mode 100644 index 0000000000..c856f39888 --- /dev/null +++ b/frappe/core/doctype/language/language.json @@ -0,0 +1,160 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 1, + "autoname": "field:language_code", + "beta": 0, + "creation": "2014-08-22 16:12:17.249590", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 0, + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "language_code", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Language Code", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "language_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Language Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "flag", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Flag", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "based_on", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Based On", + "length": 0, + "no_copy": 0, + "options": "Language", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "icon-globe", + "idx": 0, + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2016-08-23 15:06:32.827148", + "modified_by": "Administrator", + "module": "Core", + "name": "Language", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "search_fields": "language_name", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "language_name", + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/language/language.py b/frappe/core/doctype/language/language.py new file mode 100644 index 0000000000..f108dd5749 --- /dev/null +++ b/frappe/core/doctype/language/language.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, json +from frappe.model.document import Document + +class Language(Document): + pass + +def export_languages_json(): + '''Export list of all languages''' + languages = frappe.db.get_all('Language', fields=['name', 'language_name']) + languages = [{'name': d.language_name, 'code': d.name} for d in languages] + + languages.sort(lambda a,b: 1 if a['code'] > b['code'] else -1) + + with open(frappe.get_app_path('frappe', 'geo', 'languages.json'), 'w') as f: + f.write(frappe.as_json(languages)) + +def sync_languages(): + '''Sync frappe/geo/languages.json with Language''' + with open(frappe.get_app_path('frappe', 'geo', 'languages.json'), 'r') as f: + data = json.loads(f.read()) + + for l in data: + if not frappe.db.exists('Language', l['code']): + frappe.get_doc({ + 'doctype': 'Language', + 'language_code': l['code'], + 'language_name': l['name'] + }).insert() + +def update_language_names(): + '''Update frappe/geo/languages.json names (for use via patch)''' + with open(frappe.get_app_path('frappe', 'geo', 'languages.json'), 'r') as f: + data = json.loads(f.read()) + + for l in data: + frappe.db.set_value('Language', l['code'], 'language_name', l['name']) diff --git a/frappe/core/doctype/language/test_language.py b/frappe/core/doctype/language/test_language.py new file mode 100644 index 0000000000..a4f35dd77b --- /dev/null +++ b/frappe/core/doctype/language/test_language.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +# test_records = frappe.get_test_records('Language') + +class TestLanguage(unittest.TestCase): + pass diff --git a/frappe/core/doctype/page/page.py b/frappe/core/doctype/page/page.py index 8f39fdca7f..e2cea59993 100644 --- a/frappe/core/doctype/page/page.py +++ b/frappe/core/doctype/page/page.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +import os from frappe.model.document import Document from frappe.build import html_to_js_template from frappe.model.utils import render_include @@ -49,15 +50,10 @@ class Page(Document): from frappe.core.doctype.doctype.doctype import make_module_and_roles make_module_and_roles(self, "roles") - if not frappe.flags.in_import and getattr(conf,'developer_mode', 0) and self.standard=='Yes': - from frappe.modules.export_file import export_to_files - from frappe.modules import get_module_path, scrub - import os - export_to_files(record_list=[['Page', self.name]]) - - # write files - path = os.path.join(get_module_path(self.module), 'page', scrub(self.name), scrub(self.name)) + from frappe.modules.utils import export_module_json + path = export_module_json(self, self.standard=='Yes', self.module) + if path: # js if not os.path.exists(path + '.js'): with open(path + '.js', 'w') as f: @@ -132,6 +128,9 @@ class Page(Document): template = frappe.render_template(template, context) self.script = html_to_js_template(fname, template) + self.script + # flag for not caching this page + self._dynamic_page = True + if frappe.lang != 'en': from frappe.translate import get_lang_js self.script += get_lang_js("page", self.name) diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index b7f63b7c91..c5505b1b13 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -3,7 +3,9 @@ from __future__ import unicode_literals import frappe +import json from frappe import _ +import frappe.desk.query_report from frappe.utils import cint from frappe.model.document import Document from frappe.modules.export_file import export_to_files @@ -48,6 +50,40 @@ class Report(Document): make_boilerplate("controller.py", self, {"name": self.name}) make_boilerplate("controller.js", self, {"name": self.name}) + def get_data(self, filters=None, limit=None, user=None): + '''Run the report''' + out = [] + + if self.report_type in ('Query Report', 'Script Report'): + # query and script reports + data = frappe.desk.query_report.run(self.name, filters=filters, user=user) + out.append([d.split(':')[0] for d in data.get('columns')]) + out += data.get('result') + else: + # standard report + params = json.loads(self.json) + columns = params.get('columns') + filters = params.get('filters') + + def _format(parts): + # sort by is saved as DocType.fieldname, covert it to sql + return '`tab{0}`.`{1}`'.format(*parts) + + order_by = _format(params.get('sort_by').split('.')) + ' ' + params.get('sort_order') + if params.get('sort_by_next'): + order_by += ', ' + _format(params.get('sort_by_next').split('.')) + ' ' + params.get('sort_order_next') + + result = frappe.get_list(self.ref_doctype, fields = [_format([c[1], c[0]]) for c in columns], + filters=filters, order_by = order_by, as_list=True, limit=limit, user=user) + + meta = frappe.get_meta(self.ref_doctype) + + out.append([meta.get_label(c[0]) for c in columns]) + out = out + [list(d) for d in result] + + return out + + @Document.whitelist def toggle_disable(self, disable): self.db_set("disabled", cint(disable)) diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index 6cc6a6a4ab..6168672733 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -1,10 +1,30 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import frappe +from __future__ import unicode_literals +import frappe, json, os import unittest test_records = frappe.get_test_records('Report') class TestReport(unittest.TestCase): - pass + def test_report_builder(self): + if frappe.db.exists('Report', 'User Activity Report'): + frappe.delete_doc('Report', 'User Activity Report') + + with open(os.path.join(os.path.dirname(__file__), 'user_activity_report.json'), 'r') as f: + frappe.get_doc(json.loads(f.read())).insert() + + report = frappe.get_doc('Report', 'User Activity Report') + data = report.get_data() + self.assertEquals(data[0][0], 'ID') + self.assertEquals(data[0][1], 'User Type') + self.assertTrue('Administrator' in [d[0] for d in data]) + + def test_query_report(self): + report = frappe.get_doc('Report', 'Permitted Documents For User') + data = report.get_data(filters={'user': 'Administrator', 'doctype': 'DocType'}) + self.assertEquals(data[0][0], 'Name') + self.assertEquals(data[0][1], 'Module') + self.assertTrue('User' in [d[0] for d in data]) + diff --git a/frappe/core/doctype/report/user_activity_report.json b/frappe/core/doctype/report/user_activity_report.json new file mode 100644 index 0000000000..8202d1727d --- /dev/null +++ b/frappe/core/doctype/report/user_activity_report.json @@ -0,0 +1,17 @@ +{ + "add_total_row": 0, + "apply_user_permissions": 1, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "is_standard": "No", + "javascript": null, + "json": "{\"filters\":[],\"columns\":[[\"name\",\"User\"],[\"user_type\",\"User\"],[\"first_name\",\"User\"],[\"last_name\",\"User\"],[\"last_active\",\"User\"],[\"role\",\"UserRole\"]],\"sort_by\":\"User.modified\",\"sort_order\":\"desc\",\"sort_by_next\":null,\"sort_order_next\":\"desc\"}", + "modified": "2016-09-01 02:59:07.728890", + "module": "Core", + "name": "User Activity Report", + "query": null, + "ref_doctype": "User", + "report_name": "User Activity Report", + "report_type": "Report Builder" +} \ No newline at end of file diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json index 7567c24500..9e056dfe2a 100644 --- a/frappe/core/doctype/role/role.json +++ b/frappe/core/doctype/role/role.json @@ -14,13 +14,14 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "role_name", "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, - "in_list_view": 1, + "in_list_view": 0, "label": "Role Name", "length": 0, "no_copy": 0, @@ -40,6 +41,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "If disabled, this role will be removed from all users.", "fieldname": "disabled", "fieldtype": "Check", @@ -61,6 +63,33 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "1", + "fieldname": "desk_access", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Desk Access", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, @@ -74,7 +103,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-07-25 05:24:24.406260", + "modified": "2016-09-23 05:38:14.727541", "modified_by": "Administrator", "module": "Core", "name": "Role", diff --git a/frappe/core/doctype/role/test_records.json b/frappe/core/doctype/role/test_records.json index 9008a92c39..49442e6e42 100644 --- a/frappe/core/doctype/role/test_records.json +++ b/frappe/core/doctype/role/test_records.json @@ -1,14 +1,22 @@ [ { - "doctype": "Role", - "role_name": "_Test Role" - }, + "doctype": "Role", + "role_name": "_Test Role", + "desk_access": 1 + }, { - "doctype": "Role", - "role_name": "_Test Role 2" - }, + "doctype": "Role", + "role_name": "_Test Role 2", + "desk_access": 1 + }, { - "doctype": "Role", - "role_name": "_Test Role 3" + "doctype": "Role", + "role_name": "_Test Role 3", + "desk_access": 1 + }, + { + "doctype": "Role", + "role_name": "_Test Role 4", + "desk_access": 0 } ] \ No newline at end of file diff --git a/frappe/core/doctype/scheduler_log/README.md b/frappe/core/doctype/scheduler_log/README.md deleted file mode 100644 index c4691907ac..0000000000 --- a/frappe/core/doctype/scheduler_log/README.md +++ /dev/null @@ -1 +0,0 @@ -Log exceptions (errors) raised when executing the scheduler. \ No newline at end of file diff --git a/frappe/core/doctype/scheduler_log/__init__.py b/frappe/core/doctype/scheduler_log/__init__.py deleted file mode 100644 index 0e57cb68c3..0000000000 --- a/frappe/core/doctype/scheduler_log/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - diff --git a/frappe/core/doctype/scheduler_log/scheduler_log.js b/frappe/core/doctype/scheduler_log/scheduler_log.js deleted file mode 100644 index ac3b026934..0000000000 --- a/frappe/core/doctype/scheduler_log/scheduler_log.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Scheduler Log', { - refresh: function(frm) { - - } -}); diff --git a/frappe/core/doctype/scheduler_log/scheduler_log.py b/frappe/core/doctype/scheduler_log/scheduler_log.py deleted file mode 100644 index 4af039c8ee..0000000000 --- a/frappe/core/doctype/scheduler_log/scheduler_log.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe - -from frappe.model.document import Document - -class SchedulerLog(Document): - def onload(self): - if not self.seen: - self.seen = 1 - self.save() - -def set_old_logs_as_seen(): - frappe.db.sql("""update `tabScheduler Log` set seen=1 - where seen=0 and datediff(curdate(), creation) > 7""") diff --git a/frappe/core/doctype/scheduler_log/test_scheduler_log.py b/frappe/core/doctype/scheduler_log/test_scheduler_log.py deleted file mode 100644 index 39594cdd95..0000000000 --- a/frappe/core/doctype/scheduler_log/test_scheduler_log.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -import frappe -import unittest - -# test_records = frappe.get_test_records('Scheduler Log') - -class TestSchedulerLog(unittest.TestCase): - pass diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js index d6d11130a9..aa3f4a766b 100644 --- a/frappe/core/doctype/system_settings/system_settings.js +++ b/frappe/core/doctype/system_settings/system_settings.js @@ -1,5 +1,4 @@ frappe.ui.form.on("System Settings", "refresh", function(frm) { - frappe.setup_language_field(frm); frappe.call({ method: "frappe.core.doctype.system_settings.system_settings.load", callback: function(data) { diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 174ba0ba15..dc8926a49a 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -1,657 +1,736 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2014-04-17 16:53:52.640856", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2014-04-17 16:53:52.640856", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "editable_grid": 0, "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "localization", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "localization", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "country", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Country", - "length": 0, - "no_copy": 0, - "options": "Country", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "country", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Country", + "length": 0, + "no_copy": 0, + "options": "Country", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "language", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Language", - "length": 0, - "no_copy": 0, - "options": "Loading...", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "language", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Language", + "length": 0, + "no_copy": 0, + "options": "Language", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "time_zone", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Time Zone", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "time_zone", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Time Zone", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "setup_complete", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Setup Complete", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "setup_complete", + "fieldtype": "Check", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Setup Complete", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "fieldname": "date_and_number_format", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Date and Number Format", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "date_and_number_format", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Date and Number Format", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "date_format", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Date Format", - "length": 0, - "no_copy": 0, - "options": "yyyy-mm-dd\ndd-mm-yyyy\ndd/mm/yyyy\ndd.mm.yyyy\nmm/dd/yyyy\nmm-dd-yyyy", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "date_format", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Date Format", + "length": 0, + "no_copy": 0, + "options": "yyyy-mm-dd\ndd-mm-yyyy\ndd/mm/yyyy\ndd.mm.yyyy\nmm/dd/yyyy\nmm-dd-yyyy", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "column_break_7", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_7", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "number_format", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Number Format", - "length": 0, - "no_copy": 0, - "options": "#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "number_format", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Number Format", + "length": 0, + "no_copy": 0, + "options": "#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "float_precision", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Float Precision", - "length": 0, - "no_copy": 0, - "options": "\n2\n3\n4\n5\n6", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "float_precision", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Float Precision", + "length": 0, + "no_copy": 0, + "options": "\n2\n3\n4\n5\n6", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "fieldname": "background_workers", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Background Workers", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "sec_backup_limit", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Backups", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "Run scheduled jobs only if checked", - "fieldname": "enable_scheduler", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Enable Scheduled Jobs", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "3", + "description": "Older backups will be automatically deleted", + "fieldname": "backup_limit", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Number of Backups", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "scheduler_last_event", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Scheduler Last Event", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 1, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "background_workers", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Background Workers", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "fieldname": "security", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Security", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Run scheduled jobs only if checked", + "fieldname": "enable_scheduler", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Enable Scheduled Jobs", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "06:00", - "description": "Session Expiry in Hours e.g. 06:00", - "fieldname": "session_expiry", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Session Expiry", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "scheduler_last_event", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Scheduler Last Event", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 1, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "720:00", - "description": "In Hours", - "fieldname": "session_expiry_mobile", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Session Expiry Mobile", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "security", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Security", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "column_break_13", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "06:00", + "description": "Session Expiry in Hours e.g. 06:00", + "fieldname": "session_expiry", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Session Expiry", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "Note: Multiple sessions will be allowed in case of mobile device", - "fieldname": "deny_multiple_sessions", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Allow only one session per user", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "720:00", + "description": "In Hours", + "fieldname": "session_expiry_mobile", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Session Expiry Mobile", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "eg. If Apply User Permissions is checked for Report DocType but no User Permissions are defined for Report for a User, then all Reports are shown to that User", - "fieldname": "ignore_user_permissions_if_missing", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Ignore User Permissions If Missing", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_13", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "fieldname": "email", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "EMail", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Note: Multiple sessions will be allowed in case of mobile device", + "fieldname": "deny_multiple_sessions", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Allow only one session per user", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "Your organization name and address for the email footer.", - "fieldname": "email_footer_address", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Email Footer Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "eg. If Apply User Permissions is checked for Report DocType but no User Permissions are defined for Report for a User, then all Reports are shown to that User", + "fieldname": "ignore_user_permissions_if_missing", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Ignore User Permissions If Missing", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "column_break_18", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "email", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "EMail", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "disable_standard_email_footer", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Disable Standard Email Footer", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Your organization name and address for the email footer.", + "fieldname": "email_footer_address", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Email Footer Address", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_18", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "disable_standard_email_footer", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Disable Standard Email Footer", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "icon-cog", - "idx": 0, - "image_view": 0, - "in_create": 0, - "in_dialog": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2016-06-25 19:25:20.090354", - "modified_by": "Administrator", - "module": "Core", - "name": "System Settings", - "name_case": "", - "owner": "Administrator", + ], + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "icon-cog", + "idx": 0, + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2016-08-23 15:12:00.582283", + "modified_by": "Administrator", + "module": "Core", + "name": "System Settings", + "name_case": "", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "ASC", + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "sort_order": "ASC", "track_seen": 0 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/translation/test_translation.py b/frappe/core/doctype/translation/test_translation.py index af262328c9..5b4a078951 100644 --- a/frappe/core/doctype/translation/test_translation.py +++ b/frappe/core/doctype/translation/test_translation.py @@ -9,6 +9,9 @@ import unittest from frappe import _ class TestTranslation(unittest.TestCase): + def setUp(self): + frappe.db.sql('delete from tabTranslation') + def tearDown(self): frappe.local.lang = 'en' frappe.local.lang_full_dict=None @@ -19,21 +22,51 @@ class TestTranslation(unittest.TestCase): frappe.local.lang = key frappe.local.lang_full_dict=None translation = create_translation(key, val) - self.assertEquals(_(translation.source_name), val[1]) + self.assertEquals(_(val[0]), val[1]) frappe.delete_doc('Translation', translation.name) frappe.local.lang_full_dict=None - self.assertEquals(_(translation.source_name), val[0]) + + self.assertEquals(_(val[0]), val[0]) + + def test_parent_language(self): + data = [ + ['es', ['Test Data', 'datos de prueba']], + ['es', ['Test Spanish', 'prueba de español']], + ['es-MX', ['Test Data', 'pruebas de datos']] + ] + + for key, val in data: + create_translation(key, val) + + frappe.local.lang = 'es' + + frappe.local.lang_full_dict=None + self.assertTrue(_(data[0][0]), data[0][1]) + + frappe.local.lang_full_dict=None + self.assertTrue(_(data[1][0]), data[1][1]) + + frappe.local.lang = 'es-MX' + + # different translation for es-MX + frappe.local.lang_full_dict=None + self.assertTrue(_(data[2][0]), data[2][1]) + + # from spanish (general) + frappe.local.lang_full_dict=None + self.assertTrue(_(data[1][0]), data[1][1]) def get_translation_data(): - html_source_data = """ - Test Data """ - html_translated_data = """ + html_source_data = """ + Test Data""" + html_translated_data = """ testituloksia """ return {'hr': ['Test data', 'Testdaten'], 'ms': ['Test Data','ujian Data'], 'et': ['Test Data', 'testandmed'], + 'es': ['Test Data', 'datos de prueba'], 'en': ['Quotation', 'Tax Invoice'], 'fi': [html_source_data, html_translated_data]} diff --git a/frappe/core/doctype/translation/translation.js b/frappe/core/doctype/translation/translation.js index 858d8c25ca..574f099d4f 100644 --- a/frappe/core/doctype/translation/translation.js +++ b/frappe/core/doctype/translation/translation.js @@ -3,9 +3,6 @@ frappe.ui.form.on('Translation', { - before_load: function(frm) { - frappe.setup_language_field(frm); - }, language: function(frm) { frm.events.update_language_code(frm); }, diff --git a/frappe/core/doctype/translation/translation.json b/frappe/core/doctype/translation/translation.json index f69327b34d..bd243d8dd8 100644 --- a/frappe/core/doctype/translation/translation.json +++ b/frappe/core/doctype/translation/translation.json @@ -3,70 +3,22 @@ "allow_import": 1, "allow_rename": 0, "autoname": "", + "beta": 0, "creation": "2016-02-17 12:21:16.175465", "custom": 0, "docstatus": 0, "doctype": "DocType", "document_type": "Setup", + "editable_grid": 0, "fields": [ { "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "default": "", - "fieldname": "language", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Language", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, + "columns": 0, "default": "en", "fieldname": "language_code", - "fieldtype": "Data", + "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -75,6 +27,7 @@ "label": "Language Code", "length": 0, "no_copy": 0, + "options": "Language", "permlevel": 0, "precision": "", "print_hide": 0, @@ -90,6 +43,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "section_break_4", "fieldtype": "Section Break", "hidden": 0, @@ -114,6 +68,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "If your data is in HTML, please copy paste the exact HTML code with the tags.", "fieldname": "source_name", "fieldtype": "Code", @@ -140,6 +95,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "column_break_6", "fieldtype": "Column Break", "hidden": 0, @@ -164,13 +120,14 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "target_name", "fieldtype": "Code", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, - "in_list_view": 0, + "in_list_view": 1, "label": "Translated", "length": 0, "no_copy": 0, @@ -189,13 +146,14 @@ "hide_heading": 0, "hide_toolbar": 0, "idx": 0, + "image_view": 0, "in_create": 0, "in_dialog": 0, "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-03-04 15:42:07.020950", + "modified": "2016-08-24 03:48:55.525143", "modified_by": "Administrator", "module": "Core", "name": "Translation", @@ -223,9 +181,11 @@ "write": 1 } ], + "quick_entry": 0, "read_only": 0, "read_only_onload": 0, "sort_field": "modified", "sort_order": "DESC", - "title_field": "language_code" + "title_field": "source_name", + "track_seen": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/translation/translation.py b/frappe/core/doctype/translation/translation.py index d88242c718..f69ab7c6de 100644 --- a/frappe/core/doctype/translation/translation.py +++ b/frappe/core/doctype/translation/translation.py @@ -5,10 +5,11 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe.translate import clear_cache class Translation(Document): def on_update(self): - frappe.cache().hdel('lang_user_translations', self.language_code) + clear_cache() def on_trash(self): - frappe.cache().hdel('lang_user_translations', self.language_code) + clear_cache() diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index 1bdc419c8c..8ed109ff9f 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -8,14 +8,36 @@ import requests from frappe.model.delete_doc import delete_doc from frappe.utils.data import today, add_to_date from frappe import _dict -from frappe.limits import SiteExpiredError, update_limits, clear_limit +from frappe.limits import update_limits, clear_limit from frappe.utils import get_url -from frappe.installer import update_site_config from frappe.core.doctype.user.user import MaxUsersReachedError test_records = frappe.get_test_records('User') class TestUser(unittest.TestCase): + def test_user_type(self): + new_user = frappe.get_doc(dict(doctype='User', email='test-for-type@example.com', + first_name='Tester')).insert() + self.assertEquals(new_user.user_type, 'Website User') + + # role with desk access + new_user.add_roles('_Test Role 2') + new_user.save() + self.assertEquals(new_user.user_type, 'System User') + + # clear role + new_user.user_roles = [] + new_user.save() + self.assertEquals(new_user.user_type, 'Website User') + + # role without desk access + new_user.add_roles('_Test Role 4') + new_user.save() + self.assertEquals(new_user.user_type, 'Website User') + + frappe.delete_doc('User', new_user.name) + + def test_delete(self): frappe.get_doc("User", "test@example.com").add_roles("_Test Role 2") self.assertRaises(frappe.LinkExistsError, delete_doc, "Role", "_Test Role 2") @@ -122,6 +144,7 @@ class TestUser(unittest.TestCase): clear_limit('users') # def test_deny_multiple_sessions(self): + # from frappe.installer import update_site_config # clear_limit('users') # # # allow one session @@ -156,13 +179,18 @@ class TestUser(unittest.TestCase): # test_request(conn1) def test_site_expiry(self): + user = frappe.get_doc('User', 'test@example.com') + user.enabled = 1 + user.new_password = 'testpassword' + user.save() + update_limits({'expiry': add_to_date(today(), days=-1)}) frappe.local.conf = _dict(frappe.get_site_config()) frappe.db.commit() - res = requests.post(get_url(), params={'cmd': 'login', 'usr': 'test@example.com', 'pwd': 'testpassword', - 'device': 'desktop'}) + res = requests.post(get_url(), params={'cmd': 'login', 'usr': + 'test@example.com', 'pwd': 'testpassword', 'device': 'desktop'}) # While site is expired status code returned is 417 Failed Expectation self.assertEqual(res.status_code, 417) diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 101e0a2636..779552c14d 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -1,7 +1,5 @@ frappe.ui.form.on('User', { before_load: function(frm) { - frappe.setup_language_field(frm); - var update_tz_select = function(user_language) { frm.set_df_property("time_zone", "options", [""].concat(frappe.all_timezones)); } diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index a4c2dea36e..481bfe8ad4 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -1,1603 +1,1603 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "beta": 0, - "creation": "2014-03-11 14:55:00", - "custom": 0, - "description": "Represents a User in the system.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 0, + "allow_copy": 0, + "allow_import": 1, + "allow_rename": 1, + "beta": 0, + "creation": "2014-03-11 14:55:00", + "custom": 0, + "description": "Represents a User in the system.", + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 0, "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "sb0_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "sb0_5", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "1", - "fieldname": "enabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Enabled", - "length": 0, - "no_copy": 0, - "oldfieldname": "enabled", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Enabled", + "length": 0, + "no_copy": 0, + "oldfieldname": "enabled", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "depends_on": "enabled", - "fieldname": "section_break_3", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "enabled", + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "email", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Email", - "length": 0, - "no_copy": 1, - "oldfieldname": "email", - "oldfieldtype": "Data", - "options": "Email", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "email", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Email", + "length": 0, + "no_copy": 1, + "oldfieldname": "email", + "oldfieldtype": "Data", + "options": "Email", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "first_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "First Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "first_name", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "first_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "First Name", + "length": 0, + "no_copy": 0, + "oldfieldname": "first_name", + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "middle_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Middle Name (Optional)", - "length": 0, - "no_copy": 0, - "oldfieldname": "middle_name", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "middle_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Middle Name (Optional)", + "length": 0, + "no_copy": 0, + "oldfieldname": "middle_name", + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "fieldname": "last_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Last Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "last_name", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "fieldname": "last_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Last Name", + "length": 0, + "no_copy": 0, + "oldfieldname": "last_name", + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "full_name", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Full Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "full_name", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Full Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "default": "1", - "depends_on": "eval:doc.__islocal", - "fieldname": "send_welcome_email", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Send Welcome Email", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "default": "1", + "depends_on": "eval:doc.__islocal", + "fieldname": "send_welcome_email", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Send Welcome Email", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "unsubscribed", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Unsubscribed", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "unsubscribed", + "fieldtype": "Check", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Unsubscribed", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "column_break0", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "oldfieldtype": "Column Break", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50%", - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "column_break0", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "oldfieldtype": "Column Break", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50%", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50%" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "username", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Username", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "username", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Username", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 1 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "", - "fieldname": "language", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Language", - "length": 0, - "no_copy": 0, - "options": "Loading...", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "", + "fieldname": "language", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Language", + "length": 0, + "no_copy": 0, + "options": "Language", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "", - "fieldname": "time_zone", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Timezone", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "", + "fieldname": "time_zone", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Timezone", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "Get your globally recognized avatar from Gravatar.com", - "fieldname": "user_image", - "fieldtype": "Attach Image", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "User Image", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "Get your globally recognized avatar from Gravatar.com", + "fieldname": "user_image", + "fieldtype": "Attach Image", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "User Image", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "depends_on": "enabled", - "fieldname": "short_bio", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "More Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "enabled", + "fieldname": "short_bio", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "More Information", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "gender", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Gender", - "length": 0, - "no_copy": 0, - "oldfieldname": "gender", - "oldfieldtype": "Select", - "options": "\nMale\nFemale\nOther", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "gender", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Gender", + "length": 0, + "no_copy": 0, + "oldfieldname": "gender", + "oldfieldtype": "Select", + "options": "\nMale\nFemale\nOther", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "phone", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Phone", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "phone", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Phone", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "birth_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Birth Date", - "length": 0, - "no_copy": 1, - "oldfieldname": "birth_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "birth_date", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Birth Date", + "length": 0, + "no_copy": 1, + "oldfieldname": "birth_date", + "oldfieldtype": "Date", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "location", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Location", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "location", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Location", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "column_break_22", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "column_break_22", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "bio", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Bio", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "bio", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Bio", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "mute_sounds", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Mute Sounds", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "mute_sounds", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Mute Sounds", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "depends_on": "eval:doc.enabled && (!doc.__islocal || !cint(doc.send_welcome_email))", - "fieldname": "change_password", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Change Password", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "eval:doc.enabled && (!doc.__islocal || !cint(doc.send_welcome_email))", + "fieldname": "change_password", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Change Password", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "new_password", - "fieldtype": "Password", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Set New Password", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "new_password", + "fieldtype": "Password", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Set New Password", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "depends_on": "eval:!doc.__islocal", - "fieldname": "send_password_update_notification", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Send Password Update Notification", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval:!doc.__islocal", + "fieldname": "send_password_update_notification", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Send Password Update Notification", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "reset_password_key", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Reset Password Key", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "reset_password_key", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Reset Password Key", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "redirect_url", - "fieldtype": "Small Text", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Redirect URL", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "redirect_url", + "fieldtype": "Small Text", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Redirect URL", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "depends_on": "enabled", - "fieldname": "email_settings", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Email Settings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "enabled", + "fieldname": "email_settings", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Email Settings", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "1", - "fieldname": "thread_notify", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Send Notifications for Transactions I Follow", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "default": "1", + "fieldname": "thread_notify", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Send Notifications for Transactions I Follow", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "email_signature", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Email Signature", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "email_signature", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Email Signature", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "depends_on": "enabled", - "fieldname": "background", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Desktop Background", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "enabled", + "fieldname": "background", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Desktop Background", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "background_image", - "fieldtype": "Attach", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Background Image", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "background_image", + "fieldtype": "Attach", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Background Image", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "background_style", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Background Style", - "length": 0, - "no_copy": 0, - "options": "Fill Screen\nTile", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "background_style", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Background Style", + "length": 0, + "no_copy": 0, + "options": "Fill Screen\nTile", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "depends_on": "enabled", - "description": "", - "fieldname": "sb1", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Roles", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "enabled", + "description": "", + "fieldname": "sb1", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Roles", + "length": 0, + "no_copy": 0, + "permlevel": 1, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "roles_html", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Roles HTML", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "roles_html", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Roles HTML", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "user_roles", - "fieldtype": "Table", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Roles Assigned", - "length": 0, - "no_copy": 0, - "options": "UserRole", - "permlevel": 1, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "user_roles", + "fieldtype": "Table", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Roles Assigned", + "length": 0, + "no_copy": 0, + "options": "UserRole", + "permlevel": 1, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "default": "", - "description": "", - "fieldname": "modules_access", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Modules Access", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "default": "", + "description": "", + "fieldname": "modules_access", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Modules Access", + "length": 0, + "no_copy": 0, + "permlevel": 1, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "modules_html", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Modules HTML", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "modules_html", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Modules HTML", + "length": 0, + "no_copy": 0, + "permlevel": 1, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "block_modules", - "fieldtype": "Table", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Block Modules", - "length": 0, - "no_copy": 0, - "options": "Block Module", - "permlevel": 1, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "block_modules", + "fieldtype": "Table", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Block Modules", + "length": 0, + "no_copy": 0, + "options": "Block Module", + "permlevel": 1, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "These values will be automatically updated in transactions and also will be useful to restrict permissions for this user on transactions containing these values.", - "fieldname": "sb2", - "fieldtype": "Section Break", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Defaults", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Column Break", - "permlevel": 1, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50%", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "These values will be automatically updated in transactions and also will be useful to restrict permissions for this user on transactions containing these values.", + "fieldname": "sb2", + "fieldtype": "Section Break", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Defaults", + "length": 0, + "no_copy": 0, + "oldfieldtype": "Column Break", + "permlevel": 1, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50%", + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50%" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "Enter default value fields (keys) and values. If you add multiple values for a field, the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields, go to \"Customize Form\".", - "fieldname": "defaults", - "fieldtype": "Table", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "User Defaults", - "length": 0, - "no_copy": 1, - "options": "DefaultValue", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "Enter default value fields (keys) and values. If you add multiple values for a field, the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields, go to \"Customize Form\".", + "fieldname": "defaults", + "fieldtype": "Table", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "User Defaults", + "length": 0, + "no_copy": 1, + "options": "DefaultValue", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "depends_on": "enabled", - "fieldname": "sb3", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Security Settings", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "enabled", + "fieldname": "sb3", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Security Settings", + "length": 0, + "no_copy": 0, + "oldfieldtype": "Section Break", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "1", - "fieldname": "simultaneous_sessions", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Simultaneous Sessions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "default": "1", + "fieldname": "simultaneous_sessions", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Simultaneous Sessions", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "System User", - "description": "If the user has any role checked, then the user becomes a \"System User\". \"System User\" has access to the desktop", - "fieldname": "user_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "User Type", - "length": 0, - "no_copy": 0, - "oldfieldname": "user_type", - "oldfieldtype": "Select", - "options": "System User\nWebsite User", - "permlevel": 1, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "default": "System User", + "description": "If the user has any role checked, then the user becomes a \"System User\". \"System User\" has access to the desktop", + "fieldname": "user_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "User Type", + "length": 0, + "no_copy": 0, + "oldfieldname": "user_type", + "oldfieldtype": "Select", + "options": "System User\nWebsite User", + "permlevel": 1, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "Allow user to login only after this hour (0-24)", - "fieldname": "login_after", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Login After", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "Allow user to login only after this hour (0-24)", + "fieldname": "login_after", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Login After", + "length": 0, + "no_copy": 0, + "permlevel": 1, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "Allow user to login only before this hour (0-24)", - "fieldname": "login_before", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Login Before", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "Allow user to login only before this hour (0-24)", + "fieldname": "login_before", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Login Before", + "length": 0, + "no_copy": 0, + "permlevel": 1, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "Restrict user from this IP address only. Multiple IP addresses can be added by separating with commas. Also accepts partial IP addresses like (111.111.111)", - "fieldname": "restrict_ip", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Restrict IP", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "Restrict user from this IP address only. Multiple IP addresses can be added by separating with commas. Also accepts partial IP addresses like (111.111.111)", + "fieldname": "restrict_ip", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Restrict IP", + "length": 0, + "no_copy": 0, + "permlevel": 1, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "column_break1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "oldfieldtype": "Column Break", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50%", - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "column_break1", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "oldfieldtype": "Column Break", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50%", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50%" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "last_login", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Last Login", - "length": 0, - "no_copy": 1, - "oldfieldname": "last_login", - "oldfieldtype": "Read Only", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "last_login", + "fieldtype": "Read Only", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Last Login", + "length": 0, + "no_copy": 1, + "oldfieldname": "last_login", + "oldfieldtype": "Read Only", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "last_ip", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Last IP", - "length": 0, - "no_copy": 1, - "oldfieldname": "last_ip", - "oldfieldtype": "Read Only", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "last_ip", + "fieldtype": "Read Only", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Last IP", + "length": 0, + "no_copy": 1, + "oldfieldname": "last_ip", + "oldfieldtype": "Read Only", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "last_active", - "fieldtype": "Datetime", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Last Active", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "last_active", + "fieldtype": "Datetime", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Last Active", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "Stores the JSON of last known versions of various installed apps. It is used to show release notes.", - "fieldname": "last_known_versions", - "fieldtype": "Text", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Last Known Versions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "Stores the JSON of last known versions of various installed apps. It is used to show release notes.", + "fieldname": "last_known_versions", + "fieldtype": "Text", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Last Known Versions", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "depends_on": "enabled", - "fieldname": "third_party_authentication", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Third Party Authentication", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "enabled", + "fieldname": "third_party_authentication", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Third Party Authentication", + "length": 0, + "no_copy": 0, + "permlevel": 1, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "fb_username", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Facebook Username", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "fb_username", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Facebook Username", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "fb_userid", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Facebook User ID", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "fb_userid", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Facebook User ID", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "google_userid", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Google User ID", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "google_userid", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Google User ID", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "column_break_49", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "column_break_49", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "github_userid", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Github User ID", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "github_userid", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Github User ID", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "github_username", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Github Username", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "github_username", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Github Username", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "icon-user", - "idx": 413, - "image_field": "user_image", - "image_view": 0, - "in_create": 0, - "in_dialog": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 5, - "menu_index": 0, - "modified": "2016-08-10 05:33:29.729971", - "modified_by": "Administrator", - "module": "Core", - "name": "User", - "owner": "Administrator", + ], + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "icon-user", + "idx": 413, + "image_field": "user_image", + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 5, + "menu_index": 0, + "modified": "2016-08-10 05:33:29.729972", + "modified_by": "Administrator", + "module": "Core", + "name": "User", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 1, - "print": 0, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 1, + "print": 0, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 0, + "submit": 0, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "full_name", - "sort_order": "DESC", - "title_field": "full_name", + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "search_fields": "full_name", + "sort_order": "DESC", + "title_field": "full_name", "track_seen": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 1b5249a30a..5252a98390 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -38,17 +38,22 @@ class User(Document): [m.module_name for m in frappe.db.get_all('Desktop Icon', fields=['module_name'], filters={'standard': 1}, order_by="module_name")]) + def before_insert(self): + self.flags.in_insert = True + + def after_insert(self): + self.set_default_roles() + def validate(self): self.check_demo() - self.in_insert = self.get("__islocal") - # clear new password self.__new_password = self.new_password self.new_password = "" if self.name not in STANDARD_USERS: self.validate_email_type(self.email) + self.validate_email_type(self.name) self.add_system_manager_role() self.set_system_user() self.set_full_name() @@ -70,6 +75,10 @@ class User(Document): frappe.clear_cache(user=self.name) self.send_password_notification(self.__new_password) + def has_website_permission(self, ptype, verbose=False): + """Returns true if current user is the session user""" + return self.name == frappe.session.user + def check_demo(self): if frappe.session.user == 'demo@erpnext.com': frappe.throw('Cannot change user details in demo. Please signup for a new account at https://erpnext.com', title='Not Allowed') @@ -89,6 +98,33 @@ class User(Document): if not cint(self.enabled) and getattr(frappe.local, "login_manager", None): frappe.local.login_manager.logout(user=self.name) + def set_default_roles(self): + """Set a default role if specified by rules (`default_role`) in hooks or Portal Settings + + Hooks for default roles can be set as: + + default_roles = [ + {'role': 'Customer', 'doctype':'Contact', 'email_field': 'email_id', + 'filters': {'ifnull(customer, "")': ('!=', '')}} + ] + + """ + role_found = False + for rule in frappe.get_hooks('default_roles'): + filters = {rule.get('email_field'): self.email} + if rule.get('filters'): + filters.update(rule.get('filters')) + + match = frappe.get_all(rule.get('doctype'), filters=filters, limit=1) + if match: + role_found = True + self.add_roles(rule.get('role')) + + if not role_found: + default_role = frappe.db.get_single_value('Portal Settings', 'default_role') + if default_role: + self.add_roles(default_role) + def add_system_manager_role(self): # if adding system manager, do nothing if not cint(self.enabled) or ("System Manager" in [user_role.role for user_role in @@ -116,7 +152,7 @@ class User(Document): ]) def email_new_password(self, new_password=None): - if new_password and not self.in_insert: + if new_password and not self.flags.in_insert: _update_password(self.name, new_password) if self.send_password_update_notification: @@ -124,14 +160,26 @@ class User(Document): frappe.msgprint(_("New password emailed")) def set_system_user(self): - if self.user_roles or self.name == 'Administrator': + '''Set as System User if any of the given roles has desk_access''' + if self.has_desk_access() or self.name == 'Administrator': self.user_type = 'System User' else: self.user_type = 'Website User' + def has_desk_access(self): + '''Return true if any of the set roles has desk access''' + if not self.user_roles: + return False + + return len(frappe.db.sql("""select name + from `tabRole` where desk_access=1 + and name in ({0}) limit 1""".format(', '.join(['%s'] * len(self.user_roles))), + [d.role for d in self.user_roles])) + + def share_with_self(self): if self.user_type=="System User": - frappe.share.add(self.doctype, self.name, self.name, share=1, + frappe.share.add(self.doctype, self.name, self.name, write=1, share=1, flags={"ignore_share_permission": True}) else: frappe.share.remove(self.doctype, self.name, self.name, @@ -147,7 +195,7 @@ class User(Document): def send_password_notification(self, new_password): try: - if self.in_insert: + if self.flags.in_insert: if self.name not in STANDARD_USERS: if new_password: # new password given, no email required @@ -155,12 +203,15 @@ class User(Document): if not self.flags.no_welcome_mail and self.send_welcome_email: self.send_welcome_mail_to_user() - msgprint(_("Welcome email sent")) + self.flags.email_sent = 1 + if frappe.session.user != 'Guest': + msgprint(_("Welcome email sent")) return else: self.email_new_password(new_password) except frappe.OutgoingEmailError: + print frappe.get_traceback() pass # email server not set, don't send email @@ -458,6 +509,13 @@ def update_password(new_password, key=None, old_password=None): user_doc, redirect_url = reset_user_data(user) + # get redirect url from cache + redirect_to = frappe.cache().hget('redirect_after_login', user) + if redirect_to: + redirect_url = redirect_to + frappe.cache().hdel('redirect_after_login', user) + + frappe.local.login_manager.login_as(user) if user_doc.user_type == "System User": @@ -517,7 +575,7 @@ def verify_password(password): frappe.local.login_manager.check_password(frappe.session.user, password) @frappe.whitelist(allow_guest=True) -def sign_up(email, full_name): +def sign_up(email, full_name, redirect_to): user = frappe.db.get("User", {"email": email}) if user: if user.disabled: @@ -526,9 +584,12 @@ def sign_up(email, full_name): return _("Already Registered") else: if frappe.db.sql("""select count(*) from tabUser where - HOUR(TIMEDIFF(CURRENT_TIMESTAMP, TIMESTAMP(modified)))=1""")[0][0] > 200: - frappe.msgprint("Login is closed for sometime, please check back again in an hour.") - raise Exception, "Too Many New Users" + HOUR(TIMEDIFF(CURRENT_TIMESTAMP, TIMESTAMP(modified)))=1""")[0][0] > 300: + + frappe.respond_as_web_page(_('Temperorily Disabled'), + _('Too many users signed up recently, so the registration is disabled. Please try back in an hour'), + http_status_code=429) + from frappe.utils import random_string user = frappe.get_doc({ "doctype":"User", @@ -540,7 +601,14 @@ def sign_up(email, full_name): }) user.flags.ignore_permissions = True user.insert() - return _("Registration Details Emailed.") + + if redirect_to: + frappe.cache().hset('redirect_after_login', user.name, redirect_to) + + if user.flags.email_sent: + return _("Please check your email for verification") + else: + return _("Please ask your administrator to verify your sign-up") @frappe.whitelist(allow_guest=True) def reset_password(user): @@ -630,10 +698,10 @@ def has_permission(doc, user): # dont allow non Administrator user to view / edit Administrator user return False -def notifify_admin_access_to_system_manager(login_manager=None): +def notify_admin_access_to_system_manager(login_manager=None): if (login_manager and login_manager.user == "Administrator" - and frappe.local.conf.notifify_admin_access_to_system_manager): + and frappe.local.conf.notify_admin_access_to_system_manager): message = """

{dear_system_manager}

diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py index 3f68e137eb..b689905daa 100644 --- a/frappe/core/notifications.py +++ b/frappe/core/notifications.py @@ -7,7 +7,7 @@ import frappe def get_notification_config(): return { "for_doctype": { - "Scheduler Log": {"seen": 0}, + "Error Log": {"seen": 0}, "Communication": {"status": "Open", "communication_type": "Communication"}, "ToDo": "frappe.core.notifications.get_things_todo", "Event": "frappe.core.notifications.get_todays_events", diff --git a/frappe/docs/user/fr/guides/basics/__init__.py b/frappe/core/page/background_jobs/__init__.py similarity index 100% rename from frappe/docs/user/fr/guides/basics/__init__.py rename to frappe/core/page/background_jobs/__init__.py diff --git a/frappe/core/page/background_jobs/background_jobs.html b/frappe/core/page/background_jobs/background_jobs.html new file mode 100644 index 0000000000..793dadfa28 --- /dev/null +++ b/frappe/core/page/background_jobs/background_jobs.html @@ -0,0 +1,39 @@ +

+ {% if jobs.length %} + + + + + + + + + + {% for j in jobs %} + + + + + + {% endfor %} + +
Queue / WorkerJobCreated
{{ j.queue.split(".").slice(-1)[0] }} +
+ {{ frappe.utils.encode_tags(j.job_name) }} +
+ {% if j.exc_info %} +
+
{{ frappe.utils.encode_tags(j.exc_info) }}
+
+ {% endif %} +
{{ j.creation }}
+

+ Started + Queued + Failed +

+ {% else %} +

No pending or current jobs for this site

+ {% endif %} +

Last refreshed {{ frappe.datetime.now_datetime() }}

+
\ No newline at end of file diff --git a/frappe/core/page/background_jobs/background_jobs.js b/frappe/core/page/background_jobs/background_jobs.js new file mode 100644 index 0000000000..458a8ad5ee --- /dev/null +++ b/frappe/core/page/background_jobs/background_jobs.js @@ -0,0 +1,39 @@ +frappe.pages['background_jobs'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Background Jobs', + single_column: true + }); + + $(frappe.render_template('background_jobs_outer')).appendTo(page.body); + page.content = $(page.body).find('.table-area'); + + frappe.pages.background_jobs.page = page; +} + +frappe.pages['background_jobs'].on_page_show = function(wrapper) { + frappe.pages.background_jobs.refresh_jobs(); +} + +frappe.pages.background_jobs.refresh_jobs = function() { + var page = frappe.pages.background_jobs.page; + + // don't call if already waiting for a response + if(page.called) return; + page.called = true; + frappe.call({ + method: 'frappe.core.page.background_jobs.background_jobs.get_info', + args: { + show_failed: page.body.find('.show-failed').prop('checked') ? 1 : 0 + }, + callback: function(r) { + page.called = false; + page.body.find('.list-jobs').remove(); + $(frappe.render_template('background_jobs', {jobs:r.message || []})).appendTo(page.content); + + if(frappe.get_route()[0]==='background_jobs') { + frappe.background_jobs_timeout = setTimeout(frappe.pages.background_jobs.refresh_jobs, 2000); + } + } + }); +} diff --git a/frappe/core/page/background_jobs/background_jobs.json b/frappe/core/page/background_jobs/background_jobs.json new file mode 100644 index 0000000000..6701cc54bc --- /dev/null +++ b/frappe/core/page/background_jobs/background_jobs.json @@ -0,0 +1,22 @@ +{ + "content": null, + "creation": "2016-08-18 16:44:14.322642", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2016-08-18 16:48:11.577611", + "modified_by": "Administrator", + "module": "Core", + "name": "background_jobs", + "owner": "Administrator", + "page_name": "background_jobs", + "roles": [ + { + "role": "System Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "title": "Background Jobs" +} \ No newline at end of file diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py new file mode 100644 index 0000000000..bd1abc73c0 --- /dev/null +++ b/frappe/core/page/background_jobs/background_jobs.py @@ -0,0 +1,51 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + +from rq import Queue, Worker +from frappe.utils.background_jobs import get_redis_conn +from frappe.utils import format_datetime, cint + +colors = { + 'queued': 'orange', + 'failed': 'red', + 'started': 'green' +} + +@frappe.whitelist() +def get_info(show_failed=False): + conn = get_redis_conn() + queues = Queue.all(conn) + workers = Worker.all(conn) + jobs = [] + + def add_job(j, name): + if j.kwargs.get('site')==frappe.local.site or True: + jobs.append({ + 'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \ + or str(j.kwargs.get('job_name')), + 'status': j.status, 'queue': name, + 'creation': format_datetime(j.created_at), + 'color': colors[j.status] + }) + if j.exc_info: + jobs[-1]['exc_info'] = j.exc_info + + for w in workers: + j = w.get_current_job() + if j: + add_job(j, w.name) + + for q in queues: + if q.name != 'failed': + for j in q.get_jobs(): add_job(j, q.name) + + if cint(show_failed): + for q in queues: + if q.name == 'failed': + for j in q.get_jobs()[:10]: add_job(j, q.name) + + + return jobs \ No newline at end of file diff --git a/frappe/core/page/background_jobs/background_jobs_outer.html b/frappe/core/page/background_jobs/background_jobs_outer.html new file mode 100644 index 0000000000..4da4498304 --- /dev/null +++ b/frappe/core/page/background_jobs/background_jobs_outer.html @@ -0,0 +1,12 @@ +
+

+

+ +
+

+
+ +
+
\ No newline at end of file diff --git a/frappe/core/page/data_import_tool/data_import_main.html b/frappe/core/page/data_import_tool/data_import_main.html index beae98338b..f79adda979 100644 --- a/frappe/core/page/data_import_tool/data_import_main.html +++ b/frappe/core/page/data_import_tool/data_import_main.html @@ -77,6 +77,12 @@ {%= __("Ignore encoding errors.") %} +
+ +

diff --git a/frappe/core/page/data_import_tool/data_import_tool.js b/frappe/core/page/data_import_tool/data_import_tool.js index b7f9662b8b..36e2157f7b 100644 --- a/frappe/core/page/data_import_tool/data_import_tool.js +++ b/frappe/core/page/data_import_tool/data_import_tool.js @@ -42,11 +42,15 @@ frappe.DataImportTool = Class.extend({ if(me.doctype) { // render select columns - var doctype_list = [frappe.get_doc('DocType', me.doctype)]; + var parent_doctype = frappe.get_doc('DocType', me.doctype); + parent_doctype["reqd"] = true; + var doctype_list = [parent_doctype]; + frappe.meta.get_table_fields(me.doctype).forEach(function(df) { - doctype_list.push(frappe.get_doc('DocType', df.options)); + var d = frappe.get_doc('DocType', df.options); + d["reqd"]=df.reqd; + doctype_list.push(d); }); - $(frappe.render_template("data_import_tool_columns", {doctype_list: doctype_list})) .appendTo(me.select_columns.empty()); } @@ -103,7 +107,8 @@ frappe.DataImportTool = Class.extend({ return { submit_after_import: me.page.main.find('[name="submit_after_import"]').prop("checked"), ignore_encoding_errors: me.page.main.find('[name="ignore_encoding_errors"]').prop("checked"), - overwrite: !me.page.main.find('[name="always_insert"]').prop("checked") + overwrite: !me.page.main.find('[name="always_insert"]').prop("checked"), + no_email: me.page.main.find('[name="no_email"]').prop("checked") } }, args: { 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 be66a52b4e..d77127d286 100644 --- a/frappe/core/page/data_import_tool/data_import_tool.py +++ b/frappe/core/page/data_import_tool/data_import_tool.py @@ -26,12 +26,12 @@ def get_doctype_options(): doctype = frappe.form_dict['doctype'] 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, pre_process=None): +def import_file_by_path(path, ignore_links=False, overwrite=False, submit=False, pre_process=None, no_email=True): 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: - upload(rows = read_csv_content(infile.read()), ignore_links=ignore_links, overwrite=overwrite, + upload(rows = read_csv_content(infile.read()), ignore_links=ignore_links, no_email=no_email, overwrite=overwrite, submit_after_import=submit, pre_process=pre_process) def export_csv(doctype, path): diff --git a/frappe/core/page/data_import_tool/data_import_tool_columns.html b/frappe/core/page/data_import_tool/data_import_tool_columns.html index d5a28181c7..7e579607fa 100644 --- a/frappe/core/page/data_import_tool/data_import_tool_columns.html +++ b/frappe/core/page/data_import_tool/data_import_tool_columns.html @@ -4,6 +4,7 @@
{% for f in doctype.fields %} {% if (frappe.model.no_value_type.indexOf(f.fieldtype)===-1) %} + {% doctype.reqd||(f.reqd=0);%}
diff --git a/frappe/docs/user/en/bench/resources/bench-commands-cheatsheet.md b/frappe/docs/user/en/bench/resources/bench-commands-cheatsheet.md index 8dc304cdd6..e1014311dc 100755 --- a/frappe/docs/user/en/bench/resources/bench-commands-cheatsheet.md +++ b/frappe/docs/user/en/bench/resources/bench-commands-cheatsheet.md @@ -25,9 +25,9 @@ ###Config * `bench config` - Change bench configuration - * `auto_update [on/off]` Enable/Disable auto update for bench - * `dns_multitenant [on/off]` Enable/Disable DNS Multitenancy - * `http_timeout [?]` Set http timeout + * `auto_update [on/off]` Enable/Disable auto update for bench + * `dns_multitenant [on/off]` Enable/Disable DNS Multitenancy + * `http_timeout` Set http timeout * `restart_supervisor_on_update` Enable/Disable auto restart of supervisor * `serve_default_site` Configure nginx to serve the default site on * `update_bench_on_update` Enable/Disable bench updates on running bench... @@ -44,6 +44,8 @@ * `sudoers ` Add commands to sudoers list for execution... * `supervisor ` generate config for supervisor * `add-domain ` add custom domain for site + * `firewall ` setup firewall and block all ports except 22, 80 and 443 + * `ssh-port ` change the default ssh connection port ###Development @@ -53,6 +55,7 @@ * `bench remove-from-installed-apps [app-name]` Remove app from the list of apps * `bench uninstall-app [app-name]` Delete app and everything linked to the app * `bench remove-app [app-name]` Remove app from the bench entirely +* `bench --site [sitename] --force reinstall ` Reinstall with fresh database (Caution: Will wipe out old database) * `bench new-site [sitename]` - Creates a new site * `--db-name` Database name * `--mariadb-root-username` Root username for MariaDB @@ -63,20 +66,20 @@ * `--source_sql` Initiate database with a SQL file * `--install-app` Install app after installation` * `bench use [site]` Sets a default site -* `bench drop-site` - Removes site from disk and database completely +* `bench drop-site` Removes site from disk and database completely * `--root-login` * `--root-password` -* `bench console` - Opens a IPython console in the bench venv -* `bench execute` - Execute a method inside any app. +* `bench console` Opens a IPython console in the bench venv +* `bench execute` Execute a method inside any app. * Eg : `bench execute frappe.utils.scheduler.enqueue_scheduler_events` -* `bench mysql` - Opens SQL Console -* `bench run-tests` - Run tests +* `bench mysql` Opens SQL Console +* `bench run-tests` Run tests * `--app` App Name * `--doctype` DocType to run tests for * `--test` Specific Test * `--module` Run a particular module that has tests * `--profile` Runs a Python profiler on the test -* `bench disable-production` - Disables production environment +* `bench disable-production` Disables production environment ###Scheduler diff --git a/frappe/docs/user/en/guides/app-development/exporting-customizations.md b/frappe/docs/user/en/guides/app-development/exporting-customizations.md new file mode 100644 index 0000000000..12592ecb02 --- /dev/null +++ b/frappe/docs/user/en/guides/app-development/exporting-customizations.md @@ -0,0 +1,15 @@ +# Exporting Customizations to your App + +A common use case is to extend a DocType via Custom Fields and Property Setters for a particular app. To save these settings to an app, go to **Customize Form** + +You will see a button for **Export Customizations** + + + +Here you can select the module and whether you want these particular customizations to be synced after every update. + +The customizations will be exported to a new folder `custom` in the module folder of your app. The customizations will be saved by the name of the DocType + + + +When you do `bench update` or `bench migrate` these customizations will be synced to the app. \ No newline at end of file diff --git a/frappe/docs/user/en/guides/app-development/index.txt b/frappe/docs/user/en/guides/app-development/index.txt index 2c74940479..5a9b4cc4a9 100755 --- a/frappe/docs/user/en/guides/app-development/index.txt +++ b/frappe/docs/user/en/guides/app-development/index.txt @@ -4,6 +4,7 @@ generating-docs how-enable-developer-mode-in-frappe fetch-custom-field-value-from-master-to-all-related-transactions executing-code-on-doctype-events +exporting-customizations how-to-create-custom-fields-during-app-installation insert-a-document-via-api how-to-improve-a-standard-control diff --git a/frappe/docs/user/en/guides/app-development/trigger-event-on-deletion-of-grid-row.md b/frappe/docs/user/en/guides/app-development/trigger-event-on-deletion-of-grid-row.md index 86ca8f876c..cc60f6d0b8 100755 --- a/frappe/docs/user/en/guides/app-development/trigger-event-on-deletion-of-grid-row.md +++ b/frappe/docs/user/en/guides/app-development/trigger-event-on-deletion-of-grid-row.md @@ -1,43 +1,47 @@ -To trigger an event when a row from a Child Table has been deleted (when user clicks on `delete` button), you need to add a handler the `fieldname_remove` event to Child Table, where fieldname is the fieldname of the Child Table in Parent Table decloration. - - For example: - - Assuming that your parent DocType is named `Item` has a Table Field linked to `Item Color` DocType with decloration name `color`. - +To trigger an event when a row from a Child Table has been deleted (when user clicks on `delete` button), you need to add a handler the `fieldname_remove` event to Child Table, where fieldname is the fieldname of the Child Table in Parent Table declaration. + + For example: + + Assuming that your parent DocType is named `Item` has a Table Field linked to `Item Color` DocType with decloration name `color`. + In order to "catch" the delete event: - - ```javascript - frappe.ui.form.on('Item Color', - color_remove: function(frm) { - // You code here - // If you console.log(frm.doc.color) you will get the remaining color list - } - ); - ``` - + + frappe.ui.form.on('Item Color', { + color_remove: function(frm) { + // You code here + // If you console.log(frm.doc.color) you will get the remaining color list + } + ); + The same process is used to trigger the add event (when user clicks on `add row` button): + + frappe.ui.form.on('Item Color', { + color_remove: function(frm) { + // You code here + // If you console.log(frm.doc.color) you will get the remaining color list + }, + color_add: function(frm) { + } + }); + + Notice that the handling is be made on Child DocType Table `form.ui.on` and not on Parent Doctype so a minimal full example is: + + ```javascript - frappe.ui.form.on('Item Color', - color_remove: function(frm) { - // Your code here - }, - color_add: function(frm) { - // Your code here - } - ); - ``` - - Notice that the handling is be made on Child DocType Table `form.ui.on` and not on Parent Doctype so a minimal full example is: - - - ```javascript frappe.ui.form.on('Item',{ - // Your client side handling for Item + // Your client side handling for Item }); - - frappe.ui.form.on('Item Color', + + frappe.ui.form.on('Item Color', { color_remove: function(frm) { // Deleting is triggered here } ); ``` +Handlers are: + +1. fieldname_add +1. fieldname_move +1. fieldname_before_remove +1. fieldname_remove + diff --git a/frappe/docs/user/en/guides/app-development/using-html-templates-in-javascript.md b/frappe/docs/user/en/guides/app-development/using-html-templates-in-javascript.md index e2e9123b76..e1728ae86f 100755 --- a/frappe/docs/user/en/guides/app-development/using-html-templates-in-javascript.md +++ b/frappe/docs/user/en/guides/app-development/using-html-templates-in-javascript.md @@ -1,6 +1,6 @@ Often while building javascript interfaces, there is a need to render DOM as an HTML template. Frappe Framework uses John Resig's Microtemplate script to render HTML templates in the Desk application. -> Note 1: In Frappe we use the Jinja-like `{%` tags to embed code rather than the standard `<%` +> Note 1: In Frappe we use the Jinja-like `{% raw %}{%{% endraw %}` tags to embed code rather than the standard `<%` > Note 2: Never use single quotes `'` inside the HTML template. diff --git a/frappe/docs/user/en/guides/basics/sites.md b/frappe/docs/user/en/guides/basics/sites.md index 81f51aa599..bb8918358e 100755 --- a/frappe/docs/user/en/guides/basics/sites.md +++ b/frappe/docs/user/en/guides/basics/sites.md @@ -79,4 +79,4 @@ starting it with the following command. ### Adding a new site -`frappe new-site SITENAME` +`bench new-site SITENAME` diff --git a/frappe/docs/user/en/guides/basics/writing-tests.md b/frappe/docs/user/en/guides/basics/writing-tests.md index edc7bd64b0..2cb528f3e2 100755 --- a/frappe/docs/user/en/guides/basics/writing-tests.md +++ b/frappe/docs/user/en/guides/basics/writing-tests.md @@ -29,14 +29,15 @@ If you need more information about test execution - you can use verbose log leve --test --module (Run a particular module that has tests) --profile (Runs a Python profiler on the test) + --junit-xml-output (The command provides test results in the standard XUnit XML format) #### 2.1. Example for app: All applications are located in folder: "~/frappe-bench/apps". We can run tests for each application. - frappe-bench/apps/erpnext/ - - frappe-bench/apps/erpnext_demo - - frappe-bench/apps/frappe + - frappe-bench/apps/erpnext_demo/ + - frappe-bench/apps/frappe/ bench run-tests --app erpnext bench run-tests --app erpnext_demo @@ -98,6 +99,28 @@ We should use module name like this (related to application folder) 255 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/model/base_document.py:91(get) 12 0.000 0.000 0.002 0.000 +#### 2.6. Example for XUnit XML: + +##### How to run: + + bench run-tests --junit-xml-output=/reports/junit_test.xml + +##### Example of test report: + + + + + + details about failure + + + +It’s designed for the CI Jenkins, but will work for anything else that understands an XUnit-formatted XML representation of test results. + +#### Jenkins configuration support: +1. You should install xUnit plugin - https://wiki.jenkins-ci.org/display/JENKINS/xUnit+Plugin +2. After installation open Jenkins job configuration, click the box named “Publish JUnit test result report” under the "Post-build Actions" and enter path to XML report: +(Example: _reports/*.xml_) ## 3. Tests for a DocType @@ -177,6 +200,6 @@ We should use module name like this (related to application folder) ## 4. Client Side Testing (Using Selenium) -> This feature is still under development. +This feature is still under development. -For an example see, [https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py](https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py) +For an example see, [https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py](https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py) \ No newline at end of file diff --git a/frappe/docs/user/en/guides/desk/making_charts_in_c3js.md b/frappe/docs/user/en/guides/desk/making_charts_in_c3js.md new file mode 100644 index 0000000000..91a5d9438a --- /dev/null +++ b/frappe/docs/user/en/guides/desk/making_charts_in_c3js.md @@ -0,0 +1,35 @@ +# Making Charts using c3.js + +Frappe bundles the c3.js libary to make charts inside the app and provides a wrapper class so that you can start using charts out of the box. To use chart, you need the x and y data, make a wrapper block and then just make the chart object. + +### Time Series Example + + page.chart = new frappe.ui.Chart({ + // attach the chart here + wrapper: $('
').appendTo(page.body), + + // pass the data, like + // ['x', '2016-01-01', '2016-01-02'] + // ['Value', 20, 30] + data: { + x: 'x', + xFormat: '%Y-%m-%d', + columns: [data[0], data[1]], + }, + legend: { + show: false + }, + axis: { + x: { + type: 'timeseries', + }, + y: { + min: 0, + padding: {bottom: 10} + } + } + }); + +### Help + +For more options, see the [c3js.org](http://c3js.org/examples.html) docs \ No newline at end of file diff --git a/frappe/docs/user/en/guides/integration/how_to_setup_oauth.md b/frappe/docs/user/en/guides/integration/how_to_setup_oauth.md new file mode 100644 index 0000000000..7a1d615cfe --- /dev/null +++ b/frappe/docs/user/en/guides/integration/how_to_setup_oauth.md @@ -0,0 +1,47 @@ +# How to setup oauth? + +OAuth 2.0 provider based on oauthlib is built into frappe. Third party apps can now access resources of users based on Frappe Role and User permission system. To setup an app to access + +## OAuth defines four roles + +#### resource owner +An entity capable of granting access to a protected resource. When the resource owner is a person, it is referred to as an end-user. + +#### resource server +The server hosting the protected resources, capable of accepting and responding to protected resource requests using access tokens. + +#### client +An application making protected resource requests on behalf of the resource owner and with its authorization. The term "client" does not imply any particular implementation characteristics (e.g., +whether the application executes on a server, a desktop, or other devices). + +#### authorization server +The server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization. + +## Setup OAuth Provider + +System Managers can setup behavior of confirmation message as `Force` or `Auto` in OAuth Provider Settings. +If Force is selected the system will always ask for user's confirmation. If Auto is selected system asks for the confirmation only if there are no active tokens for the user. + +Go to + +> Setup > Integrations > OAuth Provider Settings + + + +## Add a Client App + +As a System Manager go to + +> Setup > Integrations > OAuth Client + + + +To add a client fill in the following details + +1. **App Name** : Enter App Name e.g. CAVS +2. **Skip Authorization** : If this is checked, during authentication there won't be me any confirmation message +3. **Scopes** : List of scopes shown to user along with confirmation message. scopes are separated by semicolon ';' +4. **Redirect URIs** : List of Redirect URIs separated by semicolon ';' +5. **Default Redirect URIs** : Default Redirect URI from list of Redirect URIs +6. **Grant Type**: select `Authorization Code` +7. **Response Type**: select `Code` diff --git a/frappe/docs/user/en/guides/integration/index.txt b/frappe/docs/user/en/guides/integration/index.txt index 8f8d276e69..6bee5d8835 100755 --- a/frappe/docs/user/en/guides/integration/index.txt +++ b/frappe/docs/user/en/guides/integration/index.txt @@ -1 +1,3 @@ rest_api +how_to_setup_oauth +using_oauth diff --git a/frappe/docs/user/en/guides/integration/using_oauth.md b/frappe/docs/user/en/guides/integration/using_oauth.md new file mode 100644 index 0000000000..1a6f694574 --- /dev/null +++ b/frappe/docs/user/en/guides/integration/using_oauth.md @@ -0,0 +1,109 @@ +# Using OAuth + +Once the client and provider settings are entered, following steps can be used to start using OAuth 2.0 + +### Authorization Code Endpoint + +#### Authorization Request + +URL: +``` +[GET] 0.0.0.0:8000/api/method/frappe.integration_broker.oauth2.authorize +``` +Params: +``` +client_id = +scope = +response_type = "code" +redirect_uri = +``` + +#### Confirmation Dialog + + + +Click 'Allow' to receive authorization code in redirect uri. + +``` +http://localhost:3000/oauth_code?code=plkj2mqDLwaLJAgDBAkyR1W8Co08Ud +``` +If user clicks 'Deny' receive error +``` +http://localhost:3000/oauth_code?error=access_denied +``` + +### Token Endpoints + +#### Get Access Token + +URL: +``` +[POST] 0.0.0.0:8000/api/method/frappe.integration_broker.oauth2.get_token +``` +Params: +``` +grant_type = "authorization_code" +code = +redirect_uri = +client_id = +``` +Response: +``` +{ + "access_token": "pNO2DpTMHTcFHYUXwzs74k6idQBmnI", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "cp74cxbbDgaxFuUZ8Usc7egYlhKbH1", + "scope": "project" +} +``` + +#### Refresh Access Token + +URL: +``` +[POST] 0.0.0.0:8000/api/method/frappe.integration_broker.oauth2.get_token +``` +Params: +``` +grant_type = "refresh_token" +refresh_token = +redirect_uri = +client_id = +``` +Response: +``` +{ + "access_token": "Ywz1iNk0b21iAmjWAYnFWT4CuudHD5", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "PNux3Q8Citr3s9rl2zEsKuU1l8bSN5", + "scope": "project" +} +``` +#### Revoke Token Endpoint + +URL: +``` +[POST] 0.0.0.0:8000/api/method/frappe.integration_broker.oauth2.revoke_token +``` +Params: +``` +token = +``` +Success Response +``` +status : 200 + +{"message": "success"} +``` +Error Response: +``` +status : 400 + +{"message": "bad request"} +``` + +### Accessing Resource + +Add header `Authorizaton: Bearer ` to Frappe's REST API endpoints to access user's resource diff --git a/frappe/docs/user/en/guides/portal-development/building.md b/frappe/docs/user/en/guides/portal-development/building.md deleted file mode 100755 index 1e7ca23c9a..0000000000 --- a/frappe/docs/user/en/guides/portal-development/building.md +++ /dev/null @@ -1,15 +0,0 @@ -# Building the site - -To make the pages to be served on the web, they must first be synced with the database. This is done by running: - - bench --site sitename sync-www - -To re-build the site - - bench --site sitename --force sync-www - -Clearing the website cache - - bench --site sitename clear-website-cache - -{next} diff --git a/frappe/docs/user/en/guides/portal-development/contents.md b/frappe/docs/user/en/guides/portal-development/contents.md index c2ebaf5551..fc2fa6936c 100755 --- a/frappe/docs/user/en/guides/portal-development/contents.md +++ b/frappe/docs/user/en/guides/portal-development/contents.md @@ -7,11 +7,23 @@ You can also make Previous and Next buttons by adding `previous` or `next` in `{ ### Showing contents # This is a title - + Hello paragraph - + ### Contents: - + {index} {next} + +### Ordering + +You can defining the ordering of pages in index by defining the index.txt file in your folder. The index.txt file must have the names (without extensions) of the pages in that folder indicating the order. + +For example for this folder the `index.txt` looks like: + + adding-pages + ordering + contents + context + building diff --git a/frappe/docs/user/en/guides/portal-development/context.md b/frappe/docs/user/en/guides/portal-development/context.md index 5c72a1835b..d0fdc3cf63 100755 --- a/frappe/docs/user/en/guides/portal-development/context.md +++ b/frappe/docs/user/en/guides/portal-development/context.md @@ -1,8 +1,26 @@ -# Adding to context +# Dynamic Pages -You can add more data for the pages by adding a `.py` file with the same filename (e.g. `index.py` for `index.md`) with a `get_context` method. +You can render pages dynamically using Jinja templating language. To query data, you can update that `context` object that you pass to the template. +This can be done by adding a `.py` file with the same filename (e.g. `index.py` for `index.md`) with a `get_context` method. + +### Example + +If you want to show a page to see users, make a `users.html` and `users.py` file in the `www/` folder. + +In `users.py`: + + import frappe def get_context(context): - context.data = frappe.db.sql("some query") + context.users = frappe.db.sql("select first_name, last_name from `tabUser`") + +In `users.html`: + +

List of Users

+
    + {% for user in users %} +
  1. {{ user.first_name }} {{ user.last_name or "" }}
  2. + {% endfor %} +
{next} diff --git a/frappe/docs/user/en/guides/portal-development/index.md b/frappe/docs/user/en/guides/portal-development/index.md index 348e7cf9e5..a9aaab4312 100755 --- a/frappe/docs/user/en/guides/portal-development/index.md +++ b/frappe/docs/user/en/guides/portal-development/index.md @@ -1,7 +1,17 @@ # Making Portals +Frappe has powerful tools to build portals where pages can be dynamically generated using templates (Jinja) and users can be shown records after login + +#### Adding Pages + You can make your website by adding pages to the `/www` folder of your website. The urls of your site will match the path of your pages within the `/www` folder. Pages must be `.html` or `.md` (Markdown) files. Basic HTML template is provided in frappe in `frappe/templates/base_template.html` +#### Views after Login + +After logging in, the user sees a "My Account" page `/me` where user can access certain documents that are shown via a menu + +The user can view records based on permissions and also add / edit them with **Web Forms** + {index} diff --git a/frappe/docs/user/en/guides/portal-development/index.txt b/frappe/docs/user/en/guides/portal-development/index.txt index 875325806e..bf00b5d541 100755 --- a/frappe/docs/user/en/guides/portal-development/index.txt +++ b/frappe/docs/user/en/guides/portal-development/index.txt @@ -1,6 +1,6 @@ adding-pages -ordering -contents context -building generators +contents +web-forms +portal-roles diff --git a/frappe/docs/user/en/guides/portal-development/portal-roles.md b/frappe/docs/user/en/guides/portal-development/portal-roles.md new file mode 100644 index 0000000000..9c23d006c4 --- /dev/null +++ b/frappe/docs/user/en/guides/portal-development/portal-roles.md @@ -0,0 +1,25 @@ +# Portal Roles + +Version: 7.1+ + +Roles can be assigned to Website Users and they will see menu based on their role + +1. A default role can be set in **Portal Settings** +1. Each Portal Menu Item can have a role associated with it. If that role is set, then only those users having that role can see that menu item +1. Rules can be set for default roles that will be set on default users on hooks + + + +#### Rules for Default Role + +For example if the email id matches with a contact id, then set role Customer or Supplier: + + default_roles = [ + {'role': 'Customer', 'doctype':'Contact', 'email_field': 'email_id', + 'filters': {'ifnull(customer, "")': ('!=', '')}}, + {'role': 'Supplier', 'doctype':'Contact', 'email_field': 'email_id', + 'filters': {'ifnull(supplier, "")': ('!=', '')}}, + {'role': 'Student', 'doctype':'Student', 'email_field': 'student_email_id'} + ] + + diff --git a/frappe/docs/user/en/guides/portal-development/web-forms.md b/frappe/docs/user/en/guides/portal-development/web-forms.md new file mode 100644 index 0000000000..92e9c603ac --- /dev/null +++ b/frappe/docs/user/en/guides/portal-development/web-forms.md @@ -0,0 +1,11 @@ +# Customizing Web Forms + +Web Forms are a powerful way to add forms to your website. Web forms are powerful and scriptable and from Version 7.1+ they also include tables, paging and other utilties + + + +### Standard Web Forms + +If you check on the "Is Standard" checkbox, a new folder will be created in the `module` of the Web Form for that web form. In this folder, you will see a `.py` and `.js` file that you can customize the web form with. + +{next} \ No newline at end of file diff --git a/frappe/docs/user/en/guides/reports-and-printing/print-format-for-reports.md b/frappe/docs/user/en/guides/reports-and-printing/print-format-for-reports.md index dd582169c0..b979654d86 100755 --- a/frappe/docs/user/en/guides/reports-and-printing/print-format-for-reports.md +++ b/frappe/docs/user/en/guides/reports-and-printing/print-format-for-reports.md @@ -65,6 +65,6 @@ Here is what the report looks like: 1. [Bootstrap Stylesheet](http://getbootstrap.com) is pre-loaded. 1. You can use all global functions like `fmt_money` and dateutil. 1. Translatable strings should be written as `__("text")` -1. You can create modules and import using `{% include "templates/includes/formats/common_format" %}` +1. You can create modules and import using `{% raw %}{% include "templates/includes/formats/common_format" %}{% endraw %}` \ No newline at end of file diff --git a/frappe/docs/user/en/tutorial/task-runner.md b/frappe/docs/user/en/tutorial/task-runner.md index cb704c12bd..fcf520fedb 100755 --- a/frappe/docs/user/en/tutorial/task-runner.md +++ b/frappe/docs/user/en/tutorial/task-runner.md @@ -66,7 +66,7 @@ Here we can point to a Python function and that function will be executed every articles_transacted.append(d.article) -We can place the above code code in any accessible Python module. The route is defined in `hooks.py`, so for our purposes we would place this code in `library_management/tasks.py`. +We can place the above code in any accessible Python module. The route is defined in `hooks.py`, so for our purposes we would place this code in `library_management/tasks.py`. Note: diff --git a/frappe/docs/user/en/tutorial/users-and-records.md b/frappe/docs/user/en/tutorial/users-and-records.md index 61e6b6dec1..394899a795 100755 --- a/frappe/docs/user/en/tutorial/users-and-records.md +++ b/frappe/docs/user/en/tutorial/users-and-records.md @@ -28,7 +28,7 @@ First let us create a new Article: New Article -Here you will see that the the DocType you had created has been rendered as a form. The validations and other rules will also apply as designed. Let us fill out one Article. +Here you will see that the DocType you had created has been rendered as a form. The validations and other rules will also apply as designed. Let us fill out one Article. New Article diff --git a/frappe/docs/user/fr/guides/app-development/adding-module-icons-on-desktop.md b/frappe/docs/user/fr/guides/app-development/adding-module-icons-on-desktop.md deleted file mode 100644 index e6e0156bf6..0000000000 --- a/frappe/docs/user/fr/guides/app-development/adding-module-icons-on-desktop.md +++ /dev/null @@ -1,34 +0,0 @@ -To create a module icon for a Page, List or Module, you will have to edit the `config/desktop.py` file in your app. - -In this file you will have to write the `get_data` method that will return a dict object with the module icon parameters - -### Example 1: Module Icon - - def get_data(): - return { - "Accounts": { - "color": "#3498db", - "icon": "octicon octicon-repo", - "type": "module" - }, - } - -### Example 2: List Icon - - def get_data(): - return { - "To Do": { - "color": "#f1c40f", - "icon": "icon-check", - "icon": "octicon octicon-check", - "label": _("To Do"), - "link": "List/ToDo", - "doctype": "ToDo", - "type": "list" - }, - } - - -Note: Module views are visible based on permissions. - - \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/app-development/custom-module-icon.md b/frappe/docs/user/fr/guides/app-development/custom-module-icon.md deleted file mode 100644 index 609f7f3803..0000000000 --- a/frappe/docs/user/fr/guides/app-development/custom-module-icon.md +++ /dev/null @@ -1,21 +0,0 @@ -If you want to create a custom icon for your module, you will have to create an SVG file for your module and set the path to this file in the `desktop/config.py` of your app.
- -This icon is loaded via AJAX first time, then it will be rendered. - -Example: - - from frappe import _ - - def get_data(): - return { - "Frappe Apps": { - "color": "orange", - "icon": "assets/frappe/images/frappe.svg", - "label": _("Frappe.io Portal"), - "type": "module" - } - } - -> PS: A great place to buy SVG icons for a low cost is the awesome [Noun Project](http://thenounproject.com/) - - \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/app-development/dialogs-types.md b/frappe/docs/user/fr/guides/app-development/dialogs-types.md deleted file mode 100644 index 6b17121dd0..0000000000 --- a/frappe/docs/user/fr/guides/app-development/dialogs-types.md +++ /dev/null @@ -1,118 +0,0 @@ -Frappe provide a group of standard dialogs that are very usefull while coding. - -## Alert Dialog - -![Alert](/files/show_alert.png) - -Is helpfull for show a non-obstrutive message. - -This dialog have 2 parameters `txt`that is the message and `seconds` that is the time that the message will be showed for the user, the standard is `3 seconds`. - -### Example - - show_alert('Hi, do you have a new message', 5); - ---- - -## Prompt Dialog - -![Prompt](/files/promp_dialog.png) - -Is helpful for ask a value for the user - -This dialog have 4 parameters, they are: - -- **fields:** a list with the fields objects -- **callback:** the function that manage the received values -- **title:** the title of the dialog -- **primary_label:** the label of the primary button - -### Example - - frappe.prompt([ - {'fieldname': 'birth', 'fieldtype': 'Date', 'label': 'Birth Date', 'reqd': 1} - ], - function(values){ - show_alert(values, 5); - }, - 'Age verification', - 'Subscribe me' - ) - ---- -## Confirm Dialog - -![Confirm](/files/confirm_dialog.png) - -Usefull to get a confirmation from the user before do an action - -This dialog have 3 arguments, they are: - -- **mesage:** The message content -- **onyes:** The callback on positive confirmation -- **oncancel:** The callback on negative confirmation - -### Example - - frappe.confirm( - 'Are you sure to leave this page?', - function(){ - window.close(); - }, - function(){ - show_alert('Thanks for continue here!') - } - ) - ---- - -## Message Print - -![MSGPrint](/files/msgprint_dialog.png) - -Is helpfull for show a informational dialog for the user; - -This dialog have 2 arguments, they are: - -- **message:** The message content, can be a HTML string too -- **title:** The title of the dialog - -### Example - - msgprint("Server Status" - + "
" - + "
    " - + "
  • 28% Memory
  • " - + "
  • 12% Processor
  • " - + "
  • 0.3% Disk
  • " - "
", 'Server Info') - ---- - -### Custom Dialog - -![Class](/files/dialog_constructor.png) - -Frappé provide too a `Class` that you can extend and build your own custom dialogs - -`frappe.ui.Dialog` - -### Example - - var d = new frappe.ui.Dialog({ - 'fields': [ - {'fieldname': 'ht', 'fieldtype': 'HTML'}, - {'fieldname': 'today', 'fieldtype': 'Date', 'default': frappe.datetime.nowdate()} - ], - primary_action: function(){ - d.hide(); - show_alert(d.get_values()); - } - }); - d.fields_dict.ht.$wrapper.html('Hello World'); - d.show(); - - - - - \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/app-development/executing-code-on-doctype-events.md b/frappe/docs/user/fr/guides/app-development/executing-code-on-doctype-events.md deleted file mode 100644 index a5b57e0920..0000000000 --- a/frappe/docs/user/fr/guides/app-development/executing-code-on-doctype-events.md +++ /dev/null @@ -1,29 +0,0 @@ -To execute code when a DocType is inserted, validated (before saving), updated, submitted, cancelled, deleted, you must write in the DocType's controller module. - -#### 1. Controller Module - -The controller module exists in the `doctype` folder in the Module of the `DocType`. For example, the controller for **ToDo** exists in `frappe/desk/doctype/todo/todo.py` (version 5). A controller template is created when the DocType is created. which looks like - - from __future__ import unicode_literals - - import frappe - from frappe.model.document import Document - - class CustomType(Document): - pass - -#### 2. Document Properties - -All the fields and child tables are available to the class as attributes. For example the **name** property is `self.name` - -#### 3. Adding Methods - -In this module, you can add standard methods to the class that are called when a document of that type is created. Standard Handlers are: - -1. `autoname`: Called while naming. You can set the `self.name` property in the method. -1. `before_insert`: Called before a document is inserted. -1. `validate`: Called before document is saved. You can throw an exception if you don't want the document to be saved -1. `on_update`: Called after the document is inserted or updated in the database. -1. `on_submit`: Called after submission. -1. `on_cancel`: Called after cancellation. -1. `on_trash`: Called after document is deleted. diff --git a/frappe/docs/user/fr/guides/app-development/fetch-custom-field-value-from-master-to-all-related-transactions.md b/frappe/docs/user/fr/guides/app-development/fetch-custom-field-value-from-master-to-all-related-transactions.md deleted file mode 100644 index d6e495ca1e..0000000000 --- a/frappe/docs/user/fr/guides/app-development/fetch-custom-field-value-from-master-to-all-related-transactions.md +++ /dev/null @@ -1,15 +0,0 @@ -# Fetch a Field Value from a Document into a Transaction - -Let's say, there is a custom field "VAT Number" in Supplier, which should be fetched in Purchase Order. - -#### Steps: - -1. Create a Custom Field **VAT Number** for *Supplier* document with *Field Type* as **Data**. - - -1. Create another Custom Field **VAT Number** for *Purchase Order* document, but in this case with *Field Type* as **Read Only** or check **Read Only** checkbox. Set the **Options** as `supplier.vat_number`. - - -1. Go to the user menu and click "Reload". -1. Now, on selection of Supplier in a new Purchase Order, **VAT Number** will be fetched automatically from the selected Supplier. - diff --git a/frappe/docs/user/fr/guides/app-development/generating-docs.md b/frappe/docs/user/fr/guides/app-development/generating-docs.md deleted file mode 100644 index ca3d92a61d..0000000000 --- a/frappe/docs/user/fr/guides/app-development/generating-docs.md +++ /dev/null @@ -1,110 +0,0 @@ -# Generating Documentation Website for your App - -Frappe version 6.7 onwards includes a full-blown documentation generator so that you can easily create a website for your app that has both user docs and developers docs (auto-generated). These pages are generated as static HTML pages so that you can add them as GitHub pages. - -## Writing Docs - -### 1. Setting up docs - -#### 1.1. Setup `docs.py` - -The first step is to setup the docs folder. For that you must create a new file in your app `config/docs.py` if it is not auto-generated. In your `docs.py` file, add the following module properties. - - - source_link = "https://github.com/[orgname]/[reponame]" - docs_base_url = "https://[orgname].github.io/[reponame]" - headline = "This is what my app does" - sub_heading = "Slightly more details with key features" - long_description = """(long description in markdown)""" - - def get_context(context): - # optional settings - - # context.brand_html = 'Brand info on the top left' - # context.favicon = 'path to favicon' - # - # context.top_bar_items = [ - # {"label": "About", "url": context.docs_base_url + "/about"}, - # ] - - pass - -#### 1.2. Generate `/docs` - -To generate the docs for the `current` version, go to the command line and write - - bench --site sitename make-docs app_name current - -If you want to maintain versions of your docs, then you can add a version number instead of `current` - -This will create a `/docs` folder in your app. - -### 2. Add User Documentation - -To add user documentation, add folders and pages in your `/docs/user` folder in the same way you would build a website pages in the `www` folder. - -Some quick tips: - -1. Add your pages as `.md` or `.html` pages -2. Optionally add `.css` files with the same name that will be automatically served -3. Add index by adding `{index}` - -### 3. Linking - -While linking make sure you add `{{ docs_base_url }}` to all your links. - - - {% raw %}Link Description{% endraw %} - - -### 4. Adding Images - -You can add images in the `/docs/assets` folder. You can add links to the images as follows: - - {% raw %}{% endraw %} - ---- - -## Setting up output docs - -1. Create a `/docs` folder in your bench, parallel to your `/sites` folder. e.g. `/frappe-bench/docs` -2. Copy the repo of your app to the docs folder. e.g. `cp -R apps/myapp docs/myapp` (you can also clone it here using `git clone`). -3. Go to your docs folder `cd docs/myapp` -4. Checkout the gh-pages branch `git checkout --orphan gh-pages` -5. Clean everything `git rm -rf .` - -Note > The branch name `gh-pages` is only if you are using GitHub. If you are hosting this on any other static file server, can just skip this and just create your docs folder for your app like `docs/myapp` - -Putting it all together: - - mkdir docs - cp -R apps/myapp docs/myapp - cd docs/myapp - git checkout --orphan gh-pages - git rm -rf . - ---- - -## Viewing Locally - -To test your docs locally, add a `--local` option to the `build-docs` command. - - bench --site [site] build-docs [appname] --local - -Then it will build urls so that you can view these files locally. To view them locally in your browser, you can use the Python SimpleHTTPServer - -Run this from your `docs/myapp` folder: - - python -m SimpleHTTPServer 8080 - ---- - -## Publishing to GitHub Pages - -To publish your docs on GitHub pages, you will have to create an empty and orphan branch in your repository called `gh-pages` and write yours there - -Now go back to your bench folder and write these docs to this branch. For example if your path to the `gh-pages` repo is `docs/reponame` - - bench --site [site] build-docs [appname] - -To check your documentation online go to: https://[orgname].github.io/[reponame] diff --git a/frappe/docs/user/fr/guides/app-development/how-enable-developer-mode-in-frappe.md b/frappe/docs/user/fr/guides/app-development/how-enable-developer-mode-in-frappe.md deleted file mode 100644 index 5c47385fbd..0000000000 --- a/frappe/docs/user/fr/guides/app-development/how-enable-developer-mode-in-frappe.md +++ /dev/null @@ -1,15 +0,0 @@ -When you are in application design mode and you want the changes in your DocTypes, Reports etc to affect the app repository, you must be in **Developer Mode**. - -To enable developer mode, update the `site_config.json` file of your site in the sites folder for example: - - frappe-bench/sites/site1/site_config.json - -Add this to the JSON object - - "developer_mode": 1 - -After setting developer mode, clear the cache: - - $ bench clear-cache - - diff --git a/frappe/docs/user/fr/guides/app-development/how-to-create-custom-fields-during-app-installation.md b/frappe/docs/user/fr/guides/app-development/how-to-create-custom-fields-during-app-installation.md deleted file mode 100644 index 20b7fdce30..0000000000 --- a/frappe/docs/user/fr/guides/app-development/how-to-create-custom-fields-during-app-installation.md +++ /dev/null @@ -1,20 +0,0 @@ -Your custom app can automatically add **Custom Fields** to DocTypes outside of your app when it is installed to a new site. - -To do this, add the new custom fields that your app requires, using the Frappe web application. - -In your `hooks.py` file, add `"Custom Fields"` - - fixtures = ["Custom Field"] - -Export fixtures before you commit your app with: - - $ bench --site mysite export-fixtures - -This will create a new folder called `fixtures` in your app folder and a `.csv` or `.json` file will be created with the definition of the custom fields you added. - -This file will be automatically imported when the app is installed in a new site or updated via `bench update`. - -Note: You can also add single DocTypes like "Website Settings" as fixtures - - - diff --git a/frappe/docs/user/fr/guides/app-development/how-to-improve-a-standard-control.md b/frappe/docs/user/fr/guides/app-development/how-to-improve-a-standard-control.md deleted file mode 100644 index bfbf320612..0000000000 --- a/frappe/docs/user/fr/guides/app-development/how-to-improve-a-standard-control.md +++ /dev/null @@ -1,102 +0,0 @@ -Frappé has a couple of elegant and useful widgets, but some times we need to edit them to add small improvements. This small article will describe how to add new resources to the standard widgets. - -Let me explain first our goal: - -> Add `many` alternative translations in `numerous records` and in a `lot of doctypes` - -Look the highlighted sections in the __goal__, we have _many translations_ to add in _many records_ and in _many doctypes_, so, we heave a **many of work**, so we have a lot to do right? - -The answer for this question is: _-Of course not! Because we know that if one element exists in many records and in many doctypes, this element is the `Control` or `Widget`_ - -So, what we need do, is improve your goal based on the `Control`, to reduce our quantity of work. - -But, where will we find this magic element, the control? _-For now, we can look it in the JavaScript sources - let's look now at [Github](https://github.com/frappe/frappe/blob/develop/frappe/public/js/frappe/form/control.js#L13)_ - -> Don't worry if you don't understand the code for now, our goal there is simplify our work. - -Let's go ahead with the thought! - -We know where we need to make the changes, but how will we dismember which are the controls that are affected by our feature and which aren't ? - -We need to keep in mind, that `Control` are instance of `DocFields` and the `DocFields` have a field that is very important for us in this case, the field that will help us to dismember which are affected by our feature and which aren't is the field `options` in the `DocField`. - -_-Wait!, we understood that the field `options` can help us, but, how will it help us?_ - -Good question, we will define a word to put in the `options` of the `DocFields` that we need to include the feature, this world will be **`Translatable`.** - -> If you forget how to customize the options of a field look [this article](https://kb.erpnext.com/kb/customize/creating-custom-link-field), it can refresh your knowledge. - -Well, with the defined word in `options` of our selected `DocFields`, now is time to code: - -_-At last, we think we would never stop talking!_ - - frappe.ui.form.ControlData = frappe.ui.form.ControlData.$extend({ - make_input: function(){ - var options = this.df.options; - if (!options || options!=="Translatable"){ - this._super(); - return; - } - var me = this; - $('').prependTo(this.input_area); - this.$input_area = $(this.input_area); - this.$input = this.$input_area.find('input'); - this.$btn = this.$input_area.find('.dialog-btn'); - this.set_input_attributes(); - this.$input.on("focus", function(){ - me.$btn.toggle(true); - }); - this.$input.on("blur", function(){ - setTimeout(function(){ me.$btn.toggle(false) }, 500); - }); - this.input = $this.input.get(0); - this.has_input = true; - var me = this; - this.setup_button(); - }, - setup_button: function(){ - var me = this; - if (this.only_input){ - this.$btn.remove(); - return; - } - this.$btn.on("click", function(){ - var value = me.get_value(); - var options = me.df.options; - if (value && options && options==="Translatable"){ - this.open_dialog(); - } - }); - }, - open_dialog: function(){ - var doc = this.doc; - if (!doc.__unsaved){ - new frappe.ui.form.TranslationSelector({ - doc: doc, - df: this.doc, - text: this.value - }); - } - } - }); - -_-Other letter soup, for my gosh!_ - -In fact, it IS a soup of letters, for a newbie, but we are not a beginner. - -Let me explain what this code does; - - - At line 1 the code overrides the `ControlData` by one extended `Class` of itself. - - The method `make_input` checks if the docfield is **`Translatable`** to make the new `Control` if not, it calls the *original* `make_input` using `_super()` - - The method `setup_button` checks if the docfield is **`Translatable`** to enable it show a `dialog` - - The method `open_dialog` invokes a new instance of the `TranslationSelector` that we will create in the code below. - - - - \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/app-development/index.md b/frappe/docs/user/fr/guides/app-development/index.md deleted file mode 100644 index b04ae83043..0000000000 --- a/frappe/docs/user/fr/guides/app-development/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# App Development - -{index} diff --git a/frappe/docs/user/fr/guides/app-development/index.txt b/frappe/docs/user/fr/guides/app-development/index.txt deleted file mode 100644 index 2c74940479..0000000000 --- a/frappe/docs/user/fr/guides/app-development/index.txt +++ /dev/null @@ -1,14 +0,0 @@ -adding-module-icons-on-desktop -custom-module-icon -generating-docs -how-enable-developer-mode-in-frappe -fetch-custom-field-value-from-master-to-all-related-transactions -executing-code-on-doctype-events -how-to-create-custom-fields-during-app-installation -insert-a-document-via-api -how-to-improve-a-standard-control -overriding-link-query-by-custom-script -single-type-doctype -trigger-event-on-deletion-of-grid-row -dialogs-types -using-html-templates-in-javascript diff --git a/frappe/docs/user/fr/guides/app-development/insert-a-document-via-api.md b/frappe/docs/user/fr/guides/app-development/insert-a-document-via-api.md deleted file mode 100644 index aab15b4ac9..0000000000 --- a/frappe/docs/user/fr/guides/app-development/insert-a-document-via-api.md +++ /dev/null @@ -1,53 +0,0 @@ -You can insert documents via a script using the `frappe.get_doc` method - -### Examples: - -#### 1. Insert a ToDo - - todo = frappe.get_doc({"doctype":"ToDo", "description": "test"}) - todo.insert() - ---- - -#### 2. Insert without the user's permissions being checked: - - todo = frappe.get_doc({"doctype":"ToDo", "description": "test"}) - todo.insert(ignore_permissions = True) - - ---- - -#### 3. Submit after inserting - - todo = frappe.get_doc({"doctype":"ToDo", "description": "test"}) - todo.insert(ignore_permissions=True) - todo.submit() - ---- - -#### 4. Insert a document on saving of another document - - class MyType(Document): - def on_update(self): - todo = frappe.get_doc({"doctype":"ToDo", "description": "test"}) - todo.insert() - ----- - -#### 5. Insert a document with child tables: - - sales_order = frappe.get_doc({ - "doctype": "Sales Order", - "company": "_Test Company", - "customer": "_Test Customer", - "delivery_date": "2013-02-23", - "sales_order_details": [ - { - "item_code": "_Test Item Home Desktop 100", - "qty": 10.0, - "rate": 100.0, - "warehouse": "_Test Warehouse - _TC" - } - ] - }) - sales_order.insert() diff --git a/frappe/docs/user/fr/guides/app-development/overriding-link-query-by-custom-script.md b/frappe/docs/user/fr/guides/app-development/overriding-link-query-by-custom-script.md deleted file mode 100644 index cc8c8058fd..0000000000 --- a/frappe/docs/user/fr/guides/app-development/overriding-link-query-by-custom-script.md +++ /dev/null @@ -1,87 +0,0 @@ -You can override the standard link query by using `set_query` - -### 1. Adding Fitlers - -You can add filters to the query: - - frappe.ui.form.on("Bank Reconciliation", "onload", function(frm) { - cur_frm.set_query("bank_account", function() { - return { - "filters": { - "account_type": "Bank", - "group_or_ledger": "Ledger" - } - }; - }); - }); - -A more complex query: - - frappe.ui.form.on("Bank Reconciliation", "onload", function(frm){ - cur_frm.set_query("bank_account", function(){ - return { - "filters": [ - ["Bank Account": "account_type", "=", "Bank"], - ["Bank Account": "group_or_ledger", "!=", "Group"] - ] - } - }); - }); - ---- - -### 2. Calling a Different Method to Generate Results - -You can also set a server side method to be called on the query: - - frm.set_query("item_code", "items", function() { - return { - query: "erpnext.controllers.queries.item_query", - filters: frm.doc.enquiry_type === "Maintenance" ? - {"is_service_item": "Yes"} : {"is_sales_item": "Yes"} - }; - }); - - - -#### Custom Method - -The custom method should return a list of items for auto select. If you want to send additional data, you can send multiple columns in the list. - -Parameters to the custom method are: - -`def custom_query(doctype, txt, searchfield, start, page_len, filters)` - -**Example:** - - # searches for leads which are not converted - def lead_query(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select name, lead_name, company_name from `tabLead` - where docstatus < 2 - and ifnull(status, '') != 'Converted' - and ({key} like %(txt)s - or lead_name like %(txt)s - or company_name like %(txt)s) - {mcond} - order by - if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), - if(locate(%(_txt)s, lead_name), locate(%(_txt)s, lead_name), 99999), - if(locate(%(_txt)s, company_name), locate(%(_txt)s, company_name), 99999), - name, lead_name - limit %(start)s, %(page_len)s""".format(**{ - 'key': searchfield, - 'mcond':get_match_cond(doctype) - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len - }) - - - -For more examples see: - -[https://github.com/frappe/erpnext/blob/develop/erpnext/controllers/queries.py](https://github.com/frappe/erpnext/blob/develop/erpnext/controllers/queries.py) - - \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/app-development/single-type-doctype.md b/frappe/docs/user/fr/guides/app-development/single-type-doctype.md deleted file mode 100644 index ee5c24b492..0000000000 --- a/frappe/docs/user/fr/guides/app-development/single-type-doctype.md +++ /dev/null @@ -1,9 +0,0 @@ -DocTypes have a table associated with them. For example DocType **Customer** will have a table `tabCustomer` associated with it. - -**Single** type DocTypes have no table associated and there is only one Document for it. This is similar to the Singleton pattern in Java. Single DocTypes are ideal for saving Settings (that are globally applicable) and for wizard / helper type forms that have no documents, but when the DocType is used for the Form UI. - -The data in Single DocType is stored in `tabSingles` (`doctype`, `field`, `value`) - -#### Examples - -In Frappe, Single types are **System Settings** and **Customize Form** \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/app-development/trigger-event-on-deletion-of-grid-row.md b/frappe/docs/user/fr/guides/app-development/trigger-event-on-deletion-of-grid-row.md deleted file mode 100644 index 68f1b77129..0000000000 --- a/frappe/docs/user/fr/guides/app-development/trigger-event-on-deletion-of-grid-row.md +++ /dev/null @@ -1,22 +0,0 @@ - -To trigger an event when a row from a grid has been deleted, you need to add a handler the `fieldname_remove` event, where fieldname is the fieldname of the grid (Table) - -

Example for add

- -

Recalculate totals when a Journal Entry row has been added

- - frappe.ui.form.on("Journal Entry Account", "accounts_add", function(frm){ - cur_frm.cscript.update_totals(frm.doc); - }); - - - -

Example for delete

- -

Recalculate totals when a Journal Entry row has been deleted

- - frappe.ui.form.on("Journal Entry Account", "accounts_remove", function(frm){ - cur_frm.cscript.update_totals(frm.doc); - }); - - \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/app-development/using-html-templates-in-javascript.md b/frappe/docs/user/fr/guides/app-development/using-html-templates-in-javascript.md deleted file mode 100644 index 374b8d79fb..0000000000 --- a/frappe/docs/user/fr/guides/app-development/using-html-templates-in-javascript.md +++ /dev/null @@ -1,39 +0,0 @@ -Often while building javascript interfaces, there is a need to render DOM as an HTML template. Frappe Framework uses John Resig's Microtemplate script to render HTML templates in the Desk application. - -> Note 1: In Frappe we use the Jinja-like `{%` tags to embed code rather than the standard `<%` - -> Note 2: Never use single quotes `'` inside the HTML template. - -To render a template, - -1. Create a template `html` file in your app. e.g. `address_list.html` -1. Add it to `build.json` for your app (you can include it in `frappe.min.js` or your own javascript file). -1. To render it in your app, use `frappe.render(frappe.templates.address_list, {[context]})` - -#### Example Template: - -From `erpnext/public/js/templates/address_list.js` - - {% raw %}

- {% for(var i=0, l=addr_list.length; i - - {%= __("Edit") %} -

{%= addr_list[i].address_type %}

-
-
- {% if(addr_list[i].is_primary_address) { %} - {%= __("Primary") %}{% } %} - {% if(addr_list[i].is_shipping_address) { %} - {%= __("Shipping") %}{% } %} -
-

{%= addr_list[i].display %}

-
- {% } %} - {% if(!addr_list.length) { %} -

{%= __("No address added yet.") %}

- {% } %}{% endraw %} - - - \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/basics/apps.md b/frappe/docs/user/fr/guides/basics/apps.md deleted file mode 100644 index f79a9f4642..0000000000 --- a/frappe/docs/user/fr/guides/basics/apps.md +++ /dev/null @@ -1,109 +0,0 @@ -# Frappe Apps - -Frappe Apps are Python packages which use the Frappe platform. They can live -anywhere on the [Python -path](https://docs.python.org/2/tutorial/modules.html#the-module-search-path) -and must have an entry in the `apps.txt` file. - - -### Creating an app - -Frappe ships with a boiler plate for a new app. The command `bench make-app -app-name` helps you start a new app by starting an interactive shell. - - - % bench make-app sample_app - App Name: sample_app - App Title: Sample App - App Description: This is a sample app. - App Publisher: Acme Inc. - App Icon: icon-linux - App Color: #6DAFC9 - App Email: info@example.com - App URL: http://example.com - App License: MIT - -The above command would create an app with the following directory structure. - - sample_app - ├── license.txt - ├── MANIFEST.in - ├── README.md - ├── sample_app - │   ├── __init__.py - │   ├── sample_app - │   │   └── __init__.py - │   ├── config - │   │   ├── desktop.py - │   │   └── __init__.py - │   ├── hooks.py - │   ├── modules.txt - │   ├── patches.txt - │   └── templates - │   ├── generators - │   │   └── __init__.py - │   ├── __init__.py - │   ├── pages - │   │   └── __init__.py - │   └── statics - └── setup.py - -Here, "App Icon" is a font awesome class that you can select from -[http://fortawesome.github.io/Font-Awesome/icons/](http://fortawesome.github.io/Font-Awesome/icons/). - -The boiler plate contains just enough files to show your app icon on the [Desk]. - -### Files in the app - -#### `hooks.py` - -The `hooks.py` file defines the metadata of your app and integration points -with other parts of Frappe or Frappe apps. Examples of such parts include task -scheduling or listening to updates to different documents in the system. For -now, it just contains the details you entered during app creation. - - - app_name = "sample-app" - app_title = "Sample App" - app_publisher = "Acme Inc." - app_description = "This is a sample app." - app_icon = "fa-linux" - app_color = "black" - app_email = "info@example.com" - app_url = "http://example.com" - app_version = 0.0.1 - -#### `modules.txt` - -Modules in Frappe help you organize Documents in Frappe and they are defined in -the `modules.txt` file in your app. It is necessary for every [DocType] to be -attached to a module. By default a module by the name of your app is added. -Also, each module gets an icon on the [Desk]. For example, the [ERPNext] app is -organized in the following modules. - - accounts - buying - home - hr - manufacturing - projects - selling - setup - stock - support - utilities - contacts - -### Adding app to a site - -Once you have an app, whether it's the one you just created or any other you -downloaded, you are required to do the following things. - -Download the app via git - - bench get-app https://github.com/org/app_name - -Install the app to your site - - bench --site app_name install-app app_name - diff --git a/frappe/docs/user/fr/guides/basics/hooks.md b/frappe/docs/user/fr/guides/basics/hooks.md deleted file mode 100644 index 9bfaaeee3c..0000000000 --- a/frappe/docs/user/fr/guides/basics/hooks.md +++ /dev/null @@ -1,273 +0,0 @@ -# Hooks - - -Hooks are the duct tape of the Frappe system. Hooks allow you to "hook" in to -functionality and events of other parts of the Frappe system. Following are the -official hooks from Frappe. - -### Application Name and Details - -1. `app_name` - slugified name with underscores e.g. "shopping\_cart" -2. `app_title` - full title name e.g. "Frappe" -3. `app_publisher` -4. `app_description` -5. `app_version` -6. `app_icon` - font-awesome icon or image url -7. `app_color` - hex colour background of the app icon - -### Install Events - -1. `before_install` -2. `after_install` - -The above hooks are called before and after installation of the app they are in. -For example, [ERPNext](/apps/erpnext)'s hooks contains a line, - - after_install = "erpnext.setup.install.after_install" - -So, the function after\_install is imported and called after ERPNext is installed. - -Note, the `before_install` and `after_install` hooks are called with no arguments. - -### Boot Session - -After a successful login, the Frappe JS Client requests for a resource called -`bootinfo`. The `bootinfo` is available as a global in Javascript via -`frappe.boot`. By default, the `bootinfo` contains - -* System defaults -* Notification status -* Permissions -* List of icons on desktop -* User settings -* Language and timezone info - -If your app wants to modify bootinfo, it can declare a hook `boot_session`. The -value is assumed to be a dotted path to a function and is called with one -argument, bootinfo which it can modify and return. - -Eg, - - boot_session = "erpnext.startup.boot.boot_session" - -### Notification configurations - -The notification configuration hook is expected to return a Python dictionary. - - { - "for_doctype": { - "Issue": {"status":"Open"}, - "Customer Issue": {"status":"Open"}, - }, - "for_module_doctypes": { - "ToDo": "To Do", - "Event": "Calendar", - "Comment": "Messages" - }, - "for_module": { - "To Do": "frappe.core.notifications.get_things_todo", - "Calendar": "frappe.core.notifications.get_todays_events", - "Messages": "frappe.core.notifications.get_unread_messages" - } - } - - -The above configuration has three parts, - -1. `for_doctype` part of the above configuration marks any "Issue" - or "Customer Issue" as unread if its status is Open -2. `for_module_doctypes` maps doctypes to module's unread count. -3. `for_module` maps modules to functions to obtain its unread count. The - functions are called without any argument. - -### Javascript / CSS Assets - -The following hooks allow you to bundle built assets to your app for serving. -There are two types of assets, app and web. The app assets are loaded in the -Desk and web assets are loaded in the website. - -1. `app_include_js` -2. `app_include_css` -3. `web_include_js` -4. `web_include_css` - -Eg, - - app_include_js = "assets/js/erpnext.min.js" - web_include_js = "assets/js/erpnext-web.min.js" - -Note: to create an asset bundle (eg, assets/js/erpnext.min.js) the target file -should be in build.json of your app. - -### Configuring Reports - -In the report view, you can force filters as per doctype using `dump_report_map` -hook. The hook should be a dotted path to a Python function which will be called -without any arguments. Example of output of this function is below. - - - "Warehouse": { - "columns": ["name"], - "conditions": ["docstatus < 2"], - "order_by": "name" - } - -Here, for a report with Warehouse doctype, would include only those records that -are not cancelled (docstatus < 2) and will be ordered by name. - -### Modifying Website Context - -Context used in website pages can be modified by adding -a `update_website_context` hook. This hook should be a dotted path to a function -which will be called with a context (dictionary) argument. - -### Customizing Email footer - -By default, for every email, a footer with content, "Sent via Frappe" is sent. -You can customize this globally by adding a `mail_footer` hook. The hook should -be a dotted path to a variable. - -### Session Creation Hook - -You can attach custom logic to the event of a successful login using -`on_session_creation` hook. The hook should be a dotted path to a Python -function that takes login\_manager as an argument. - -Eg, - - def on_session_creation(login_manager): - """make feed""" - if frappe.session['user'] != 'Guest': - # log to timesheet - pass - -### Website Clear Cache - -If you cache values in your views, the `website_clear_cache` allows you to hook -methods that invalidate your caches when Frappe tries to clear cache for all -website related pages. - -### Document hooks - -#### Permissions - -#### Query Permissions -You can customize how permissions are resolved for a DocType by hooking custom -permission match conditions using the `permission_query_conditions` hook. This -match condition is expected to be fragment for a where clause in an sql query. -Structure for this hook is as follows. - - - permission_query_conditions = { - "{doctype}": "{dotted.path.to.function}", - } - -The output of the function should be a string with a match condition. -Example of such a function, - - - def get_permission_query_conditions(): - return "(tabevent.event_type='public' or tabevent.owner='{user}'".format(user=frappe.session.user) - -The above function returns a fragment that permits an event to listed if it's -public or owned by the current user. - -#### Document permissions -You can hook to `doc.has_permission` for any DocType and add special permission -checking logic using the `has_permission` hook. Structure for this hook is, - - has_permission = { - "{doctype}": "{dotted.path.to.function}", - } - -The function will be passed the concerned document as an argument. It should -True or a falsy value after running the required logic. - -For Example, - - def has_permission(doc): - if doc.event_type=="Public" or doc.owner==frappe.session.user: - return True - -The above function permits an event if it's public or owned by the current user. - -#### CRUD Events - -You can hook to various CRUD events of any doctype, the syntax for such a hook -is as follows, - - doc_events = { - "{doctype}": { - "{event}": "{dotted.path.to.function}", - } - } - -To hook to events of all doctypes, you can use the follwing syntax also, - - doc_events = { - "*": { - "on_update": "{dotted.path.to.function}", - } - } - -The hook function will be passed the doc in concern as the only argument. - -##### List of events - -* `validate` -* `before_save` -* `after_save` -* `before_insert` -* `after_insert` -* `before_submit` -* `before_cancel` -* `before_update_after_submit` -* `on_update` -* `on_submit` -* `on_cancel` -* `on_update_after_submit` - - -Eg, - - doc_events = { - "Cab Request": { - "after_insert": topcab.schedule_cab", - } - } - -### Scheduler Hooks - -Scheduler hooks are methods that are run periodically in background. Structure for such a hook is, - - scheduler_events = { - "{event_name}": [ - "{dotted.path.to.function}" - ], - } - -#### Events - -* `daily` -* `daily_long` -* `weekly` -* `weekly_long` -* `monthly` -* `monthly_long` -* `hourly` -* `all` - -The scheduler events require celery, celerybeat and redis (or a supported and -configured broker) to be running. The events with suffix '\_long' are for long -jobs. The `all` event is triggered everytime (as per the celerybeat interval). - -Example, - - scheduler_events = { - "{daily}": [ - "erpnext.accounts.doctype.sales_invoice.sales_invoice.manage_recurring_invoices" - ], - "{daily_long}": [ - "erpnext.setup.doctype.backup_manager.backup_manager.take_backups_daily" - ], - } diff --git a/frappe/docs/user/fr/guides/basics/index.md b/frappe/docs/user/fr/guides/basics/index.md deleted file mode 100644 index 645bed29a7..0000000000 --- a/frappe/docs/user/fr/guides/basics/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Basics - -{index} diff --git a/frappe/docs/user/fr/guides/basics/index.txt b/frappe/docs/user/fr/guides/basics/index.txt deleted file mode 100644 index 910ba191d4..0000000000 --- a/frappe/docs/user/fr/guides/basics/index.txt +++ /dev/null @@ -1,7 +0,0 @@ -install -apps -sites -site_config -hooks -translations -writing-tests diff --git a/frappe/docs/user/fr/guides/basics/install.md b/frappe/docs/user/fr/guides/basics/install.md deleted file mode 100644 index 3afa43a37c..0000000000 --- a/frappe/docs/user/fr/guides/basics/install.md +++ /dev/null @@ -1,10 +0,0 @@ -# Installing Frappe - -## Frappe bench - -The following steps help you setup an isolated environment (bench) to run and -develop Frappe apps. A virtualenv is installed in the env directory. You can -activate it by running `source ./env/bin/activate` or use execute using -absolute/relative path (eg, `./env/bin/frappe`). - -For more info, see [Frappe Bench](https://github.com/frappe/bench/) diff --git a/frappe/docs/user/fr/guides/basics/site_config.md b/frappe/docs/user/fr/guides/basics/site_config.md deleted file mode 100644 index 983bf07563..0000000000 --- a/frappe/docs/user/fr/guides/basics/site_config.md +++ /dev/null @@ -1,44 +0,0 @@ -# Site Config - -Settings for `sites/[site]/site_config.json` - -`site_config.json` stores global settings for a particular site and is present in the site directory. Here is a list of properties you can set in `site_config.json`. - -Example: - - { - "db_name": "test_frappe", - "db_password": "test_frappe", - "admin_password": "admin", - } - -### Mandatory Settings - -- `db_name`: Database Name. -- `db_password`: Database password. - -### Optional Settings - -- `db_host`: Database host if not `localhost`. -- `admin_password`: Default Password for "Administrator". -- `mute_emails`: Stops email sending if true. -- `deny_multiple_logins`: Stop users from having more than one active session. -- `root_password`: MariaDB root password. - -### Defaut Outgoing Email Settings - -- `mail_server`: SMTP server hostname. -- `mail_port`: STMP port. -- `use_ssl`: Connect via SSL / TLS. -- `mail_login`: Login id for SMTP server. -- `mail_password`: Password for SMTP server. - -### Developer Settings - -- `developer_mode`: If developer mode is set, DocType changes are automatically updated in files. -- `disable_website_cache`: Don't cache website pages. -- `logging`: writes logs if **1**, writes queries also if set to **2**. - -### Others - -- `robots_txt`: Path to robots.txt file to be rendered when going to frappe-site.com/robots.txt diff --git a/frappe/docs/user/fr/guides/basics/sites.md b/frappe/docs/user/fr/guides/basics/sites.md deleted file mode 100644 index 81f51aa599..0000000000 --- a/frappe/docs/user/fr/guides/basics/sites.md +++ /dev/null @@ -1,82 +0,0 @@ -# Sites - -## Sites Directory - -Frappe is a multitenant platform and each tenant is called a site. Sites exist -in a directory called `sites_dir`, assumed as the current working directory when -running a frappe command or other services like Celery worker or a WSGI server. - -You can set `sites_dir` with an environment variable `SITES_DIR` or pass -`--sites_dir` option to the frappe command. - -Apart from the sites, the `sites_dir` should contain the following. - -#### apps.txt - -`apps.txt` contain a list of Python packages to treat as Frappe apps. Every -frappe app that you intend to use in you site should have an entry in this file. -Also, they should be in the `PYTHONPATH`. For more information, refer -[Frappe Apps](/help/apps). - -#### common\_site\_config.json - -`common_site_config.json` is an optional file. Configuration common to all sites -can be put in this file. - -#### assets - -Assets contain files that are required to be served for the browser client. -These generally include *.js, *.css, *.png files. This directory is auto -generated using the `bench build` command. - -#### languages.txt - -`languages.txt` is an autogenerated file which maps every language to it's code. - -## Site - -A site is a directory in `sites_dir` which represents a tenant in Frappe Platform. - - -### Directory Structure - - site - ├── locks - ├── private - │   └── backups - ├── public - │   └── files - │ └── testfile.txt - └── site_config.json - -* `locks` directory is used by the scheduler to synchronize various jobs using -the [file locking concept](http://en.wikipedia.org/wiki/File_locking). - -* `private` directory contains files that require authentication to access. -Presently, it is limited only to backups. - -* `public` directory contains files that can directly served. In the above - example, `testfile.txt` can be accessed by the URL, - http://site/files/testfile.txt - -* `site_config.json` contains site specific configuration - -### Site Config - -[See configuration options for `site_config.json`](/docs/user-guide/site_config.md) - -### Site Resolution - -While responding to an HTTP request, a site is automatically selected based on, - -* `Host` header in the HTTP request matches a site -* `X-Frappe-Site-Name` header in the HTTP request matches a site - -It is also possible to force the development server to serve a specific site by -starting it with the following command. - `bench --site SITENAME serve` - - -### Adding a new site - -`frappe new-site SITENAME` diff --git a/frappe/docs/user/fr/guides/basics/translations.md b/frappe/docs/user/fr/guides/basics/translations.md deleted file mode 100644 index e5ff22e9f2..0000000000 --- a/frappe/docs/user/fr/guides/basics/translations.md +++ /dev/null @@ -1,86 +0,0 @@ - - - -This document shows how to translations are managed in ERPNext and how to add -a new language or update translations of an existing language. - -### 1. Source - -Translatable text exists in 3 main sources: - - 1. Javascript Code Files (both framework and application) - 2. Python Code Files - 3. DocTypes (names, labels and select options) - -#### Strings in Code Files - -Strings in code files are annotated using the `_` (underscore) method - - 1. In Python it is the `frappe._` method. Example: - -`frappe._("String {0} must be translated".format(txt))` - - 2. In Javascript it is the `__` method. Example: - -`__("String {0} must be translated", [txt])` - -**Note:** If you find translatable strings are not properly annotated using the `_` -method, you can add them in the code and rebuild the translations. - -### 2. How Translations Are Picked up During Execution - -Whenever a translation is called via the _ method, the entire translation -dictionaries from all apps are built and stored in memcache. - -Based on the user preferences or request preferences, the appropriate -translations are loaded at the time of request on the server side. Or if -metadata (DocType) is queried, then the appropriate translations are appended -when the DocType data is requested. - -The underscore `_` method will replace the strings based on the available -translations loaded at the time. - -### 3. Adding New Translations - -1. To find untranslated strings, run `bench get-untranslated [lang] [path]` -1. Add the translated strings in another file in the same order -1. run `bench update-translations [lang] [path of untranslated strings] [path of translated strings]` - -### 4. Improving Translations: - -For updating translations, please go to the to [the translation portal](https://frappe.io/translator). - -If you want to do it directly via code: - -To improve an existing translation, just edit the master translation files in -the `translations` of each app - -> Please contribute your translations back to ERPNext by sending us a Pull -Request. - -### 5. Bootstrapping a New Language - -If you want to add a new language it is similar to adding new translations. You need to first export all the translations strings in one file, get them translated via Google Translate Tool or Bing Translate Tool and then import the translations into individual apps. - -**Step 1: Export to a file** - - $ bench get-untranslated [lang] [path] - -**Step 2: Translate** - -Create another file with updated translations (in the same order as the source file). For this you can use the [Google Translator Toolkit](https://translate.google.com/toolkit) or [Bing Translator](http://www.bing.com/translator/). - -**Step 3: Import your translations** - - $ bench update-translations [lang] [source path] [translated path] - -**Step 4: Update `languages.txt`** - -Add your language in `apps/languages.txt` and also `frappe/data/languages.txt` (fore new bench installs) - -**Step 5: Commit each app and push** - -A new file will be added to the `translations` folder in each app. You need to add that file and push to your repo. Then send us a pull-request. - ---- - diff --git a/frappe/docs/user/fr/guides/basics/writing-tests.md b/frappe/docs/user/fr/guides/basics/writing-tests.md deleted file mode 100644 index f151416ccd..0000000000 --- a/frappe/docs/user/fr/guides/basics/writing-tests.md +++ /dev/null @@ -1,109 +0,0 @@ -# Writing Tests - -### Introduction - -Frappe provides some basic tooling to quickly write automated tests. There are some basic rules: - -1. Test can be anywhere in your repository but must begin with `test_` and should be a `.py` file. -1. Tests must run on a site that starts with `test_`. This is to prevent accidental loss of data. -1. Test stubs are automatically generated for new DocTypes. -1. Frappe test runner will automatically build test records for dependant DocTypes identified by the `Link` type field (Foreign Key) -1. Tests can be executed using `bench run-tests` -1. For non-DocType tests, you can write simple unittests and prefix your file names with `test_`. - -### Tests for a DocType - -#### Writing DocType Tests: - -1. Records that are used for testing are stored in a file `test_records.json` in the doctype folder. [For example see the Event Tests](https://github.com/frappe/frappe/blob/develop/frappe/core/doctype/event/test_records.json). -1. Test cases are in a file named `test_[doctype].py` -1. To provide the test records (and dependencies) call `test_records = frappe.get_test_records('Event')` in your test case file. - -#### Example (for `test_records.json`): - - [ - { - "doctype": "Event", - "subject":"_Test Event 1", - "starts_on": "2014-01-01", - "event_type": "Public" - }, - { - "doctype": "Event", - "starts_on": "2014-01-01", - "subject":"_Test Event 2", - "event_type": "Private" - }, - { - "doctype": "Event", - "starts_on": "2014-01-01", - "subject":"_Test Event 3", - "event_type": "Private", - "event_individuals": [{ - "person": "test1@example.com" - }] - } - ] - - -#### Example (for `test_event.py`): - - # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors - # MIT License. See license.txt - - import frappe - import frappe.defaults - import unittest - - # load test records and dependencies - test_records = frappe.get_test_records('Event') - - class TestEvent(unittest.TestCase): - def tearDown(self): - frappe.set_user("Administrator") - - def test_allowed_public(self): - frappe.set_user("test1@example.com") - doc = frappe.get_doc("Event", frappe.db.get_value("Event", {"subject":"_Test Event 1"})) - self.assertTrue(frappe.has_permission("Event", doc=doc)) - - def test_not_allowed_private(self): - frappe.set_user("test1@example.com") - doc = frappe.get_doc("Event", frappe.db.get_value("Event", {"subject":"_Test Event 2"})) - self.assertFalse(frappe.has_permission("Event", doc=doc)) - - def test_allowed_private_if_in_event_user(self): - frappe.set_user("test1@example.com") - doc = frappe.get_doc("Event", frappe.db.get_value("Event", {"subject":"_Test Event 3"})) - self.assertTrue(frappe.has_permission("Event", doc=doc)) - - def test_event_list(self): - frappe.set_user("test1@example.com") - res = frappe.get_list("Event", filters=[["Event", "subject", "like", "_Test Event%"]], fields=["name", "subject"]) - self.assertEquals(len(res), 2) - subjects = [r.subject for r in res] - self.assertTrue("_Test Event 1" in subjects) - self.assertTrue("_Test Event 3" in subjects) - self.assertFalse("_Test Event 2" in subjects) - -#### Running Tests - -To run a test for a doctype - - bench run-tests --doctype [doctype] - -This function will build all the test dependencies and run your tests. - -### Running All Tests - -To run all tests: - - bench run-tests - ---- - -## Client Side Testing (Using Selenium) - -> This feature is still under development. - -For an example see, [https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py](https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py) diff --git a/frappe/docs/user/fr/guides/data/import-large-csv-file.md b/frappe/docs/user/fr/guides/data/import-large-csv-file.md deleted file mode 100644 index 5194ebd98d..0000000000 --- a/frappe/docs/user/fr/guides/data/import-large-csv-file.md +++ /dev/null @@ -1,23 +0,0 @@ -To import very large CSV files, you can use the bench utility `import-csv`. - -The benefit is that this is not subject to timeouts if you use the web interface. - -Here is an example: - - bench --site test.erpnext.com import-csv ~/Downloads/Activity_Type.csv - -### Help - - $ bench import-csv --help - Usage: bench import-csv [OPTIONS] PATH - - Import CSV using data import tool - - Options: - --only-insert Do not overwrite existing records - --submit-after-import Submit document after importing it - --ignore-encoding-errors Ignore encoding errors while coverting to unicode - --help Show this message and exit. - - - \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/data/index.md b/frappe/docs/user/fr/guides/data/index.md deleted file mode 100644 index e6f341b236..0000000000 --- a/frappe/docs/user/fr/guides/data/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Data Management - -{index} diff --git a/frappe/docs/user/fr/guides/data/index.txt b/frappe/docs/user/fr/guides/data/index.txt deleted file mode 100644 index 4794a002bc..0000000000 --- a/frappe/docs/user/fr/guides/data/index.txt +++ /dev/null @@ -1 +0,0 @@ -import-large-csv-file diff --git a/frappe/docs/user/fr/guides/deployment/checking-problems-in-bench.md b/frappe/docs/user/fr/guides/deployment/checking-problems-in-bench.md deleted file mode 100644 index 05fcd3d879..0000000000 --- a/frappe/docs/user/fr/guides/deployment/checking-problems-in-bench.md +++ /dev/null @@ -1,15 +0,0 @@ - - -If you're experiencing delays in scheduled jobs or they don't seem to run, you run run the following command to diagnose the issue. - -`bench doctor` - -A desirable output is like below - - - Workers online: True - Pending tasks 0 - Timed out locks: - - -We'll be adding more health checks soon. diff --git a/frappe/docs/user/fr/guides/deployment/configuring-https.md b/frappe/docs/user/fr/guides/deployment/configuring-https.md deleted file mode 100644 index 4fb90be226..0000000000 --- a/frappe/docs/user/fr/guides/deployment/configuring-https.md +++ /dev/null @@ -1,43 +0,0 @@ - -### Get the required files - -You can buy a SSL certificate from a trusted Certificate Authority or generate your own. For self signed certificates the browser will show a warning that the certificate is not trusted. - -The files required are - -* Certificate (usually with extension .crt) -* Decrypted private key - -If you have multiple certificates (primary and intermediate), you will have to concatenate them. For example, - - cat your_certificate.crt CA.crt >> certificate_bundle.crt - -Also make sure that your private key is not world readable. Generally, it is owned and readable only by root - - chown root private.key - chmod 600 private.key - -### Move the two files to an appropriate location - - mkdir /etc/nginx/conf.d/ssl - mv private.key /etc/nginx/conf.d/ssl/private.key - mv certificate_bundle.crt /etc/nginx/conf.d/ssl/certificate_bundle.crt - -### Setup nginx config - -Set the paths to the certificate and private key for your site - - bench set-ssl-certificate site1.local /etc/nginx/ssl/certificate_bundle.crt - bench set-ssl-key site1.local /etc/nginx/ssl/private.key - -### Generate nginx config - - bench setup nginx - -### Reload nginx - - service nginx reload - -or - - systemctl reload nginx # for CentOS 7 \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/deployment/email-notifications-for-failed-background-jobs.md b/frappe/docs/user/fr/guides/deployment/email-notifications-for-failed-background-jobs.md deleted file mode 100644 index 3c582762ec..0000000000 --- a/frappe/docs/user/fr/guides/deployment/email-notifications-for-failed-background-jobs.md +++ /dev/null @@ -1,23 +0,0 @@ - - -

Frappe handles failure of jobs in the following way,

1) If a job fails, (raises exception), it's logged in Scheduler Log and  logs/worker.error.log.
2) Keeps a lock file and would not run anymore if lock file is there.
3) Raises LockTimeoutError in case the lock file is more than 10 minutes old.

- -

You can configure email notification for scheduler errors. By writing a file, sites/common_site_config.json with content

- -
{
-  "celery_error_emails": {
-    "ADMINS": [
-      [
-        "Person 1",
-        "person1@example.com"
-      ],
-      [
-        "Person2 ",
-        "person2@example.com"
-      ]
-    ],
-    "SERVER_EMAIL": "exceptions@example.com"
-  }
-}
- -

One limitation is that it'll use local mailserver on port 25 to send the emails.

\ No newline at end of file diff --git a/frappe/docs/user/fr/guides/deployment/how-to-change-host-name-from-localhost.md b/frappe/docs/user/fr/guides/deployment/how-to-change-host-name-from-localhost.md deleted file mode 100644 index 39d0008ec8..0000000000 --- a/frappe/docs/user/fr/guides/deployment/how-to-change-host-name-from-localhost.md +++ /dev/null @@ -1,18 +0,0 @@ -While using a virtual machine, links within emails will be point to your host, e.g. localhost, like **http://localhost/set-password** etc. - -Frappe will automatically extract the host name from the incoming request, or from the `host_name` property from `site_config`. - -### bench set-config - -To fix this, you can use **bench set-config** to set your public IP or domain name as the host name. - -#### Example: - - bench --site mysite.com set-config host_name mysite.com - ---- - -Or edit the `frappe-bench/sites/mysite.com/site_config.json` and add a `host_name` property. - - - \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/deployment/how-to-enable-social-logins.md b/frappe/docs/user/fr/guides/deployment/how-to-enable-social-logins.md deleted file mode 100644 index 847cf78ea3..0000000000 --- a/frappe/docs/user/fr/guides/deployment/how-to-enable-social-logins.md +++ /dev/null @@ -1,67 +0,0 @@ -Use Facebook, Google or GitHub authentication to login to Frappe, and your users will be spared from remembering another password. - -The system uses the **Email ID** supplied by these services to **match with an existing user** in Frappe. If no such user is found, **a new user is created** of the default type **Website User**, if Signup is not disabled in Website Settings. Any System Manager can later change the user type from **Website User** to **System User**, so that the user can access the Desktop. - -
- Login screen with Social Logins enabled -
Login screen with Social Logins enabled
-
- -To enable these signups, you need to have **Client ID** and **Client Secret** from these authentication services for your Frappe site. The Client ID and Client Secret are to be set in Website > Setup > Social Login Keys. Here are the steps to obtain these credentials. - -> Use **https://{{ yoursite }}** if your site is HTTPS enabled. - ---- - -### Facebook - -1. Go to [https://developers.facebook.com](https://developers.facebook.com) -1. Click on Apps (topbar) > New App, fill in the form. -1. Go to Settings > Basic, set the **Contact Email** and save the changes. -1. Go to Settings > Advanced, find the field **Valid OAuth redirect URIs**, and enter: - **http://{{ yoursite }}/api/method/frappe.templates.pages.login.login\_via\_facebook** -1. Save the changes in Advance tab. -1. Go to Status & Review and switch on "Do you want to make this app and all its live features available to the general public?" -1. Go to Dashboard, click on the show button besides App Secret, and copy the App ID and App Secret into **Desktop > Website > Setup > Social Login Keys** - -
- -
- ---- - -### Google - -1. Go to [https://console.developers.google.com](https://console.developers.google.com) -1. Create a new Project and fill in the form. -1. Click on APIs & Auth > Credentials > Create new Client ID -1. Fill the form with: - - Web Application - - Authorized JavaScript origins as **http://{{ yoursite }}** - - Authorized redirect URI as - **http://{{ yoursite }}/api/method/frappe.templates.pages.login.login\_via\_google** -1. Go to the section **Client ID for web application** and copy the Client ID and Client Secret into **Desktop > Website > Setup > Social Login Keys** - -
- -
- ---- - -### GitHub - -1. Go to [https://github.com/settings/applications](https://github.com/settings/applications) -1. Click on **Register new application** -1. Fill the form with: - - Homepage URL as **http://{{ yoursite }}** - - Authorization callback URL as - **http://{{ yoursite }}/api/method/frappe.templates.pages.login.login\_via\_github** -1. Click on Register application. -1. Copy the generated Client ID and Client Secret into **Desktop > Website > Setup > Social Login Keys** - -
- -
- - \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/deployment/how-to-migrate-doctype-changes-to-production.md b/frappe/docs/user/fr/guides/deployment/how-to-migrate-doctype-changes-to-production.md deleted file mode 100644 index 35a15645ab..0000000000 --- a/frappe/docs/user/fr/guides/deployment/how-to-migrate-doctype-changes-to-production.md +++ /dev/null @@ -1,13 +0,0 @@ -#### 1. DocType / Schema Changes - -If you are in `developer_mode`, the `.json` files for each **DocType** are automatically updated. - -When you update in your production using `--latest` or `bench update`, these changes are updated in the site's schema too! - -#### 2. Permissions - -Permissions do not get updated because the user may have changed them. To update permissions, you can add a new patch in the `patches.txt` of your app. - - execute:frappe.permissions.reset_perms("[docype]") - - \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/deployment/index.md b/frappe/docs/user/fr/guides/deployment/index.md deleted file mode 100644 index 9487bbf803..0000000000 --- a/frappe/docs/user/fr/guides/deployment/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Deployment - -Deploying your apps on remote servers - -{index} diff --git a/frappe/docs/user/fr/guides/deployment/index.txt b/frappe/docs/user/fr/guides/deployment/index.txt deleted file mode 100644 index bf719dcf09..0000000000 --- a/frappe/docs/user/fr/guides/deployment/index.txt +++ /dev/null @@ -1,7 +0,0 @@ -migrations -how-to-migrate-doctype-changes-to-production -how-to-change-host-name-from-localhost -configuring-https -checking-problems-in-bench -email-notifications-for-failed-background-jobs -how-to-enable-social-logins diff --git a/frappe/docs/user/fr/guides/deployment/migrations.md b/frappe/docs/user/fr/guides/deployment/migrations.md deleted file mode 100644 index c40a2a7c4b..0000000000 --- a/frappe/docs/user/fr/guides/deployment/migrations.md +++ /dev/null @@ -1,69 +0,0 @@ -# Migrations - -A project often undergoes changes related to database schema during course of -its life. It may also be required patch existing data. Frappe bundles tools to -handle these schenarios. - -When you pull updates from any Frappe app (including Frappe), you should run -`bench migrate` to apply schema changes and data migrations if any. - -## Schema changes - -You can edit a DocType to add, remove or change fields. On saving a DocType, -a JSON file containing the DocType data is added to source tree of your app. -When you add an app to a site, the DocTypes are installed using this JSON file. -For making schema changes, it's required to set `developer_mode` in the -configuration. - -On running a sync (`bench migrate`), doctypes in the system are synced to -their latest version from the JSON files in the app. - -Note: Fields are soft deleted ie. the columns are not removed from the database -table and however, they will not be visible in the documents. This is done to -avoid any potential data loss situations and to allow you write related data -migrations which might need values from deleted fields. - -Note: Frappe doesn't support reverse schema migrations. - -## Data Migrations - -On introducing data related changes, you might want to run one off scripts to -change existing data to match expectations as per new code. - -To add a data migration to your code, you will have to write an `execute` -function to a python module and add it to `patches.txt` of your app. - -It is recommended to make a file with a patch number and name in its path and -add it to a patches package (directory) in your app. You can then add a line -with dotted path to the patch module to `patches.txt`. - -The directory structure followed in Frappe is as below - - - frappe - └── patches - └── 4_0 - └── my_awesome_patch.py - -The patch can be added to `patches.txt` by adding a line like - - frappe.patches.4_0.my_awesome_patch - -The metadata ie. DocType available in the execute function will be the latest as -per JSON files in the apps. However, you will not be able to access metadata of -any previous states of the system. - -#### One off Python statements - -You can also add one off python statements in `patches.txt` using the syntax, - execute:{python statement} - -For example, - execute:frappe.get_doc("User", "Guest").save() - -Note: All lines in patches.txt have to be unique. If you want to run a line -twice, you can make it unique by adding a distinct comment. - -For Example, - - execute:frappe.installer.make_site_dirs() #2014-02-19 diff --git a/frappe/docs/user/fr/guides/index.md b/frappe/docs/user/fr/guides/index.md deleted file mode 100644 index 9eeed8baa8..0000000000 --- a/frappe/docs/user/fr/guides/index.md +++ /dev/null @@ -1,7 +0,0 @@ -# Guides - -The Frappe Framework is a server side and client side framework and is built with the philosophy make it a "battries included" framework. It has libraries and API for everything from authentication to reports. - -In this section we will try and cover the most commonly used API on client and server side that will be useful for app development. - -{index} diff --git a/frappe/docs/user/fr/guides/index.txt b/frappe/docs/user/fr/guides/index.txt deleted file mode 100644 index d47785065a..0000000000 --- a/frappe/docs/user/fr/guides/index.txt +++ /dev/null @@ -1,7 +0,0 @@ -basics -app-development -deployment -reports-and-printing -portal-development -data -integration diff --git a/frappe/docs/user/fr/guides/integration/index.md b/frappe/docs/user/fr/guides/integration/index.md deleted file mode 100644 index b97815866a..0000000000 --- a/frappe/docs/user/fr/guides/integration/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Integrations - -{index} diff --git a/frappe/docs/user/fr/guides/integration/index.txt b/frappe/docs/user/fr/guides/integration/index.txt deleted file mode 100644 index 8f8d276e69..0000000000 --- a/frappe/docs/user/fr/guides/integration/index.txt +++ /dev/null @@ -1 +0,0 @@ -rest_api diff --git a/frappe/docs/user/fr/guides/integration/rest_api.md b/frappe/docs/user/fr/guides/integration/rest_api.md deleted file mode 100644 index 7f28f82b38..0000000000 --- a/frappe/docs/user/fr/guides/integration/rest_api.md +++ /dev/null @@ -1,283 +0,0 @@ -# REST API - -Frappe ships with an HTTP API. There are two parts of this API. - -1. Remote Procedure Calls (RPC) -2. REST - -## 1. RPC - -A request to an endpoint `/api/method/{dotted.path.to.function}` will call -a whitelisted python function. A function can be whitelisted using the -`frappe.whitelist` decorator. - -For example, Add the following to sample\_app/\_\_init\_\_.py - - @frappe.whitelist(allow_guest=True) - def ping(): - return 'pong' - -GET http://frappe.local:8000**/api/method/sample_app.ping** - -_Response:_ - - { - "message": "pong" - } - - -## 2. REST - -All documents in Frappe are available via a RESTful API with prefix -`/api/resource/`. - -### Login - -To login, you will have to send a POST request to the login method. - -POST http://frappe.local:8000**/api/method/login** - - usr=Administrator&pwd=admin - -_Response:_ - - { - "full_name": "Administrator", - "message": "Logged In" - } - - -Try to make an authenticated request - -GET http://frappe.local:8000**/api/method/frappe.auth.get\_logged\_user** - -_Response:_ - - { - "message": "Administrator" - } - - -### Listing Documents - -To list documents, the URL endpoint is `/api/resource/{doctype}` and the -expected HTTP verb is GET. - -Response is returned as JSON Object and the listing is an array in with the key `data`. - -GET http://frappe.local:8000**/api/resource/Person** - -_Response:_ - - { - "data": [ - { - "name": "000000012" - }, - { - "name": "000000008" - } - ] - } - - -#### Fields - -By default, only name field is included in the listing, to add more fields, you -can pass the fields param to GET request. The param has to be a JSON array. - -GET http://frappe.local:8000**/api/resource/Person/?fields=["name", "first\_name"]** - -_Response:_ - - { - "data": [ - { - "first_name": "Jane", - "name": "000000012" - }, - { - "first_name": "John", - "name": "000000008" - } - ] - } - - -#### Filters - -You can filter the listing using sql conditions by passing them as the `filters` -GET param. Each condition is an array of the format, [{doctype}, {field}, -{operator}, {operand}]. - -Eg, to filter persons with name Jane, pass a param `filters=[["Person", "first_name", "=", "Jane"]]` - -GET http://frappe.local:8000**/api/resource/Person/** - -_Response:_ - { - "data": [ - { - "name": "000000012" - } - ] - } - - -#### Pagination - -All listings are returned paginated by 20 items. To change the page size, you -can pass `limit_page_length`. To request succesive pages, pass `limit_start` as -per your `limit_page_length`. - -For Example, to request second page, pass `limit_start` as 20. - -GET http://frappe.local:8000**/api/resource/DocType** - -_Response:_ - - { - "data": [ - { - "name": "testdoc" - }, - { - "name": "Person" - }, - - ...... - - { - "name": "Website Template" - } - ] - } - - -GET http://frappe.local:8000**/api/resource/DocType?limit_start=20** - -_Response:_ - - { - "data": [ - { - "name": "Website Route" - }, - { - "name": "Version" - }, - { - "name": "Blog Post" - }, - - ...... - - { - "name": "Custom Field" - } - ] - } - - -### CRUD - -#### Create - -You can create a document by sending a `POST` request to the url, `/api/resource/{doctype}`. - -POST http://frappe.local:8000**/api/resource/Person** - -_Body_: - - data={"first_name": "Robert"} - -_Response:_ - - { - "data": { - "first_name": "Robert", - "last_name": null, - "modified_by": "Administrator", - "name": "000000051", - "parent": null, - "creation": "2014-05-04 17:22:38.037685", - "modified": "2014-05-04 17:22:38.037685", - "doctype": "Person", - "idx": null, - "parenttype": null, - "owner": "Administrator", - "docstatus": 0, - "parentfield": null - } - } - -#### Read - -You can get a document by its name using the url, `/api/resource/{doctype}/{name}` - -For Example, - -GET http://frappe.local:8000**/api/resource/Person/000000012** - -_Response:_ - - { - "data": { - "first_name": "Jane", - "last_name": "Doe", - "modified_by": "Administrator", - "name": "000000012", - "parent": null, - "creation": "2014-04-25 17:56:51.105372", - "modified": "2014-04-25 17:56:51.105372", - "doctype": "Person", - "idx": null, - "parenttype": null, - "owner": "Administrator", - "docstatus": 0, - "parentfield": null - } - } - -### Update - -You can create a document by sending a `PUT` request to the url, -`/api/resource/{doctype}`. This acts like a `PATCH` HTTP request in which you do -not have to send the whole document but only the parts you want to change. - -For Example, - -PUT http://frappe.local:8000**/api/resource/Person/000000008** - -_Body:_ - - data={"last_name": "Watson"} - -_Response:_ - - { - "data": { - "first_name": "John ", - "last_name": "Watson", - "modified_by": "Administrator", - "name": "000000008", - "creation": "2014-04-25 17:26:22.728327", - "modified": "2014-05-04 18:21:45.385995", - "doctype": "Person", - "owner": "Administrator", - "docstatus": 0 - } - } - -### Delete - -You can delete a document by its name by sending a `DELETE` request to the url, -`/api/resource/{doctype}/{name}`. - -For Example, - -DELETE http://frappe.local:8000**/api/resource/Person/000000008** - -_Response:_ - - {"message":"ok"} diff --git a/frappe/docs/user/fr/guides/portal-development/adding-pages.md b/frappe/docs/user/fr/guides/portal-development/adding-pages.md deleted file mode 100644 index c8bdd53552..0000000000 --- a/frappe/docs/user/fr/guides/portal-development/adding-pages.md +++ /dev/null @@ -1,39 +0,0 @@ -# Adding Pages - -To add pages, just add `.html` or `.md` files in the `www` folder. The pages must only have the content, not the `` and `` tags. - -You can also write markdown pages - -### Index - -The first file in a folder must be called `index.md` or `index.html` - -Either file must be present for the system to make this a valid folder to build pages. - -### Markdown - - # This is a title - - This is some page content - a [link](/link/to/page) - -### Adding Links - -Links urls to pages can be given without the `.html` extension for example `/home/link` - -### Title - -The first `

` block if present will be the page title if not specified in a special tag. If no `

` or title is specified, the file name will be the title. - -### Adding CSS - -You can also add a `.css` file with the same filename (e.g. `index.css` for `index.md`) that will be rendered with the page. - -### Special Tags - -1. `` will make the page render in Jinja -2. `` will add a custom title -3. `` will not add breadcrumbs in the page -4. `` will enable caching (if you have used Jinja templating) - -{next} diff --git a/frappe/docs/user/fr/guides/portal-development/building.md b/frappe/docs/user/fr/guides/portal-development/building.md deleted file mode 100644 index 1e7ca23c9a..0000000000 --- a/frappe/docs/user/fr/guides/portal-development/building.md +++ /dev/null @@ -1,15 +0,0 @@ -# Building the site - -To make the pages to be served on the web, they must first be synced with the database. This is done by running: - - bench --site sitename sync-www - -To re-build the site - - bench --site sitename --force sync-www - -Clearing the website cache - - bench --site sitename clear-website-cache - -{next} diff --git a/frappe/docs/user/fr/guides/portal-development/contents.md b/frappe/docs/user/fr/guides/portal-development/contents.md deleted file mode 100644 index c2ebaf5551..0000000000 --- a/frappe/docs/user/fr/guides/portal-development/contents.md +++ /dev/null @@ -1,17 +0,0 @@ -# Table of Contents - -You can add a table of contents by adding `{index}` string on a new line. - -You can also make Previous and Next buttons by adding `previous` or `next` in `{}` - -### Showing contents - - # This is a title - - Hello paragraph - - ### Contents: - - {index} - -{next} diff --git a/frappe/docs/user/fr/guides/portal-development/context.md b/frappe/docs/user/fr/guides/portal-development/context.md deleted file mode 100644 index 5c72a1835b..0000000000 --- a/frappe/docs/user/fr/guides/portal-development/context.md +++ /dev/null @@ -1,8 +0,0 @@ -# Adding to context - -You can add more data for the pages by adding a `.py` file with the same filename (e.g. `index.py` for `index.md`) with a `get_context` method. - - def get_context(context): - context.data = frappe.db.sql("some query") - -{next} diff --git a/frappe/docs/user/fr/guides/portal-development/index.md b/frappe/docs/user/fr/guides/portal-development/index.md deleted file mode 100644 index 348e7cf9e5..0000000000 --- a/frappe/docs/user/fr/guides/portal-development/index.md +++ /dev/null @@ -1,7 +0,0 @@ -# Making Portals - -You can make your website by adding pages to the `/www` folder of your website. The urls of your site will match the path of your pages within the `/www` folder. - -Pages must be `.html` or `.md` (Markdown) files. Basic HTML template is provided in frappe in `frappe/templates/base_template.html` - -{index} diff --git a/frappe/docs/user/fr/guides/portal-development/index.txt b/frappe/docs/user/fr/guides/portal-development/index.txt deleted file mode 100644 index 3eeece6502..0000000000 --- a/frappe/docs/user/fr/guides/portal-development/index.txt +++ /dev/null @@ -1,5 +0,0 @@ -adding-pages -ordering -contents -context -building diff --git a/frappe/docs/user/fr/guides/portal-development/ordering.md b/frappe/docs/user/fr/guides/portal-development/ordering.md deleted file mode 100644 index 76db949fbb..0000000000 --- a/frappe/docs/user/fr/guides/portal-development/ordering.md +++ /dev/null @@ -1,13 +0,0 @@ -# Ordering - -You can defining the ordering of pages in index by defining the index.txt file in your folder. The index.txt file must have the names (without extensions) of the pages in that folder indicating the order. - -For example for this folder the `index.txt` looks like: - - adding-pages - ordering - contents - context - building - -{next} diff --git a/frappe/docs/user/fr/guides/reports-and-printing/getting-information-from-another-document-in-print-format.md b/frappe/docs/user/fr/guides/reports-and-printing/getting-information-from-another-document-in-print-format.md deleted file mode 100644 index 321028d7af..0000000000 --- a/frappe/docs/user/fr/guides/reports-and-printing/getting-information-from-another-document-in-print-format.md +++ /dev/null @@ -1,7 +0,0 @@ -In a print format, you can get data from another document. For example in if you have a fields called `sales_order` in Sales Invoice, then you can get the sales order details using `frappe.get_doc`: - -{% raw %} - {% set sales_order_doc = frappe.get_doc("Sales Order", sales_order) %} - - {{ sales_order_doc.customer }} -{% endraw %} diff --git a/frappe/docs/user/fr/guides/reports-and-printing/how-to-make-query-report.md b/frappe/docs/user/fr/guides/reports-and-printing/how-to-make-query-report.md deleted file mode 100644 index 92ea7d4b2e..0000000000 --- a/frappe/docs/user/fr/guides/reports-and-printing/how-to-make-query-report.md +++ /dev/null @@ -1,56 +0,0 @@ -You can create tabulated reports using complex SQL queries by creating a new Report. These reports can be created by a System Manager and are stored in the Database - -> Note: You will need System Manager Permissions for this. - -To create a new Query Report: - -### 1. Create a new Report - -![Query Report](/assets/frappe_io/images/how-to/query-report.png) - -1. Set type as "Query Report" -1. Set the reference DocType - Users that have access to the reference DocType will have access to the report -1. Set the module - The report will appear in the "Custom Reports" section of the module. -1. Add your Query - -### 2. Set the Query - -You can define complex queries such as: - - - SELECT - `tabProduction Order`.name as "Production Order:Link/Production Order:200", - `tabProduction Order`.creation as "Date:Date:120", - `tabProduction Order`.production_item as "Item:Link/Item:150", - `tabProduction Order`.qty as "To Produce:Int:100", - `tabProduction Order`.produced_qty as "Produced:Int:100" - FROM - `tabProduction Order` - WHERE - `tabProduction Order`.docstatus=1 - AND ifnull(`tabProduction Order`.produced_qty,0) < `tabProduction Order`.qty - AND EXISTS (SELECT name from `tabStock Entry` where production_order =`tabProduction Order`.name) - -1. To format the columns, set labels for each column in the format: [Label]:[Field Type]/[Options]:[Width] - -### 3. Check the Report - -![Query Report](/assets/frappe_io/images/how-to/query-report-out.png) - -### 4. Advanced (adding filters) - -If you are making a standard report, you can add filters in your query report just like [script reports](https://frappe.io/kb/reports/how-to-make-script-reports) by adding a `.js` file in your query report folder. To include filters in your query, use `%(filter_key)s` where your filter value will be shown. - -For example - - SELECT ... FROM ... WHERE item_code = %(item_code)s ORDER BY ... - ---- - -### Note: Standard Script Report - -If you are developing a standard report for an app, make sure to set "Is Standard" as "Yes" - - - - \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/reports-and-printing/how-to-make-script-reports.md b/frappe/docs/user/fr/guides/reports-and-printing/how-to-make-script-reports.md deleted file mode 100644 index 4f097b34d5..0000000000 --- a/frappe/docs/user/fr/guides/reports-and-printing/how-to-make-script-reports.md +++ /dev/null @@ -1,51 +0,0 @@ -# Script Report - -You can create tabulated reports using server side scripts by creating a new Report. - -> Note: You will need Administrator Permissions for this. - -Since these reports give you unrestricted access via Python scripts, they can only be created by Administrators. The script part of the report becomes a part of the repository of the application. If you have not created an app, [read this](/developers/guide). - -> Note: You must be in [Developer Mode](/developers/how-to/enable-developer-mode) to do this - -### 1. Create a new Report - -![Query Report](/assets/frappe_io/images/how-to/script-report.png) - -1. Set Report Type as "Script Report" -1. Set "Is Standard" as "Yes" -1. Select the Module in which you want to add this report -1. In the module folder (for example if it is Accounts in ERPnext the folder will be `erpnext/accounts/report/[report-name]`) you will see that templates for the report files will be created. -1. In the `.js` file, you can set filters for the reports -1. In the `.py` file, you can write the script that will generate the report - -### 2. Add Filters - -You can add filters in the `.js`. See an example below: - - frappe.query_reports["Accounts Receivable"] = { - "filters": [ - { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("company") - }, - ] - } - -1. These properties are the same as you would set in a DocField in a DocType - -### 3. Add the Script - -In the `.py` file you can add the script for generating the report. - -1. In the `execute` method, two lists `columns` and `data` are returned -2. Columns must be a list of labels in the same format as query report. **[Label]:[Field Type]/[Options]:[Width]**. For example `Item:Link/Item:150` -3. You can use all server side modules to build your report. -4. For example see existing reports. ([Balance Sheet](https://github.com/frappe/erpnext/blob/develop/erpnext/accounts/report/balance_sheet/balance_sheet.py)) - -### 4. Commit and Push the app - -Don't forget to commit and push your app. diff --git a/frappe/docs/user/fr/guides/reports-and-printing/index.md b/frappe/docs/user/fr/guides/reports-and-printing/index.md deleted file mode 100644 index 8c464f691c..0000000000 --- a/frappe/docs/user/fr/guides/reports-and-printing/index.md +++ /dev/null @@ -1 +0,0 @@ -# Reports and Printing diff --git a/frappe/docs/user/fr/guides/reports-and-printing/index.txt b/frappe/docs/user/fr/guides/reports-and-printing/index.txt deleted file mode 100644 index ec34e2b102..0000000000 --- a/frappe/docs/user/fr/guides/reports-and-printing/index.txt +++ /dev/null @@ -1,5 +0,0 @@ -how-to-make-script-reports -how-to-make-query-report -getting-information-from-another-document-in-print-format -where-do-i-find-standard-print-formats -print-format-for-reports diff --git a/frappe/docs/user/fr/guides/reports-and-printing/print-format-for-reports.md b/frappe/docs/user/fr/guides/reports-and-printing/print-format-for-reports.md deleted file mode 100644 index dd582169c0..0000000000 --- a/frappe/docs/user/fr/guides/reports-and-printing/print-format-for-reports.md +++ /dev/null @@ -1,70 +0,0 @@ -# Report Print Formats - -In version 4.1 we introduce Report Print Formats. These are HTML templates that you can use to format Query Report data for printing. - -### 1. Creating New Print Formats - -To create a new Print Format, just drop in a `.html` file in the folder of the query report. For example, for the [General Ledger](https://github.com/frappe/erpnext/tree/develop/erpnext/accounts/report/general_ledger) report in ERPNext, you can drop in a file called `general_ledger.html` along side the `.js` and `.py` files. - -##### Tree Of `erpnext/accounts/general_ledger` - - general_ledger/ - ├── __init__.py - ├── general_ledger.html - ├── general_ledger.js - ├── general_ledger.json - └── general_ledger.py - - -### 2. Templating - -For templating, we use an adapted version of [John Resig's microtemplating script](http://ejohn.org/blog/javascript-micro-templating/). If you know Javascript, it is very easy to follow this templating language. - -##### Here are some examples (from John Resig's Blog): - -Example: Properities: - -
"> -
- -
-
- -
-
- -Example: Code structures, Loops - - <% for ( var i = 0; i < users.length; i++ ) { %> -
  • <%=users[i].name%>
  • - <% } %> - -> **Note**: It is important to note that you should not use single quotes (') in your template as the engine cannot handle them effectively. - -### 3. Data - -Data is available to the template as: - -- `data`: this is a list of records, with each record as an object with slugified properties from labels. For example "Posting Date" becomes "posting_date" -- `filters`: filters set in the report -- `report`: reportview object - -### 4. Example - -Here is how the General Ledger Report is built: - -[General Ledger Print Format Template](https://github.com/frappe/erpnext/blob/develop/erpnext/accounts/report/general_ledger/general_ledger.html) - -Here is what the report looks like: - -![General Ledger](/assets/frappe_io/images/how-to/general-ledger.png) - -##### Comments: - -1. [Bootstrap Stylesheet](http://getbootstrap.com) is pre-loaded. -1. You can use all global functions like `fmt_money` and dateutil. -1. Translatable strings should be written as `__("text")` -1. You can create modules and import using `{% include "templates/includes/formats/common_format" %}` - - \ No newline at end of file diff --git a/frappe/docs/user/fr/guides/reports-and-printing/where-do-i-find-standard-print-formats.md b/frappe/docs/user/fr/guides/reports-and-printing/where-do-i-find-standard-print-formats.md deleted file mode 100644 index 00e351c615..0000000000 --- a/frappe/docs/user/fr/guides/reports-and-printing/where-do-i-find-standard-print-formats.md +++ /dev/null @@ -1,40 +0,0 @@ -Standard Print formats are auto generated from the layout of the DocType. You can customize the standard format by -
    -
    - -

    1. Customizing Standard Print

    -Go to Setup > Customize > Customize Form View and you can: -
    -
      -
    1. Re-arranging fields by dragging and dropping
    2. -
    3. Add static elements by adding HTML type fields and adding your HTML in Options - -
    4. -
    5. Hiding fields by setting the Print Hide property
    6. -
    -
    - -

    2. Creating new layouts based on Print Formats

    - -

    As there are not templates that are generated for standard Print Formats, you will have to create new templates from scratch using the Jinja Templating Language via

    -

    Setup > Printing and Branding > Print Format - -

    -
      -
    1. See Print Format help. -
      -
    2. -
    3. You can use the Bootstrap CSS framework to layout your print formats -
      -
    4. -
    -
    -

    Tip: You can import Standard Template macros for building your print formats. - -

    -

    Example, adding the standard header: -
    -

    -
    {% raw %}{%- from "templates/print_formats/standard_macros.html" import add_header -%}
    -{{ add_header() }}{% endraw %}
    -
    diff --git a/frappe/docs/user/fr/index.txt b/frappe/docs/user/fr/index.txt index 1e50854610..4d8a2e24ae 100644 --- a/frappe/docs/user/fr/index.txt +++ b/frappe/docs/user/fr/index.txt @@ -1,3 +1 @@ tutorial -guides -videos diff --git a/frappe/docs/user/fr/videos/index.md b/frappe/docs/user/fr/videos/index.md deleted file mode 100644 index 883c586cc7..0000000000 --- a/frappe/docs/user/fr/videos/index.md +++ /dev/null @@ -1,9 +0,0 @@ -# Video Tutorials for Frappe Framework - -This 10-part video tutorial will teach you how to build complex apps in Frappe - -Pre-requisites: You must have some understanding of Python, Javascript and MySQL before you start this tutorial. - ---- - - diff --git a/frappe/docs/user/index.md b/frappe/docs/user/index.md index a95a166ace..ed43eba5a8 100644 --- a/frappe/docs/user/index.md +++ b/frappe/docs/user/index.md @@ -2,5 +2,6 @@ Select your language -1. [English](en) -1. [Français](fr) +1. [English]({{docs_base_url}}/user/en) +1. [Français]({{docs_base_url}}/user/fr) +1. [Português]({{docs_base_url}}/user/pt) diff --git a/frappe/docs/user/index.txt b/frappe/docs/user/index.txt index 7e6d9e4012..fb7f722f82 100644 --- a/frappe/docs/user/index.txt +++ b/frappe/docs/user/index.txt @@ -1,2 +1,3 @@ en fr +pt diff --git a/frappe/docs/user/fr/guides/portal-development/__init__.py b/frappe/docs/user/pt/__init__.py similarity index 100% rename from frappe/docs/user/fr/guides/portal-development/__init__.py rename to frappe/docs/user/pt/__init__.py diff --git a/frappe/docs/user/pt/index.md b/frappe/docs/user/pt/index.md new file mode 100755 index 0000000000..a25785e59e --- /dev/null +++ b/frappe/docs/user/pt/index.md @@ -0,0 +1,3 @@ +# Develop Apps with Frappe + +{index} diff --git a/frappe/docs/user/pt/index.txt b/frappe/docs/user/pt/index.txt new file mode 100755 index 0000000000..4d8a2e24ae --- /dev/null +++ b/frappe/docs/user/pt/index.txt @@ -0,0 +1 @@ +tutorial diff --git a/frappe/docs/user/fr/guides/reports-and-printing/__init__.py b/frappe/docs/user/pt/tutorial/__init__.py similarity index 100% rename from frappe/docs/user/fr/guides/reports-and-printing/__init__.py rename to frappe/docs/user/pt/tutorial/__init__.py diff --git a/frappe/docs/user/pt/tutorial/app.md b/frappe/docs/user/pt/tutorial/app.md new file mode 100755 index 0000000000..dd1a2597e3 --- /dev/null +++ b/frappe/docs/user/pt/tutorial/app.md @@ -0,0 +1,9 @@ +# O que é uma aplicação + +Uma Aplicação em Frappe é apenas uma aplicação padrão em Python. Você pode estruturar uma aplicação Frappe da mesma forma que estrutura uma aplicação padrão do Python. Para fazer o deploy, Frappe usa o padrão Setuptools do Python, assim você pode facilmente portar e instalar o aplicativo em qualquer máquina. + +Frappe Framework fornece uma interface WSGI e para o desenvolvimento você pode usar o servidor embutido Werkzeug. Para a implementação em produção, recomendamos o uso do nginx e gunicorn. + +Frappe também tem uma arquitetura multi-tenant, a partir da base. Isso significa que você pode executar vários "sites" em sua configuração, cada um poderia estar servindo um conjunto diferente de aplicativos e usuários. O banco de dados de cada site é separado. + +{next} diff --git a/frappe/docs/user/pt/tutorial/before.md b/frappe/docs/user/pt/tutorial/before.md new file mode 100755 index 0000000000..31f407a7d2 --- /dev/null +++ b/frappe/docs/user/pt/tutorial/before.md @@ -0,0 +1,47 @@ +# Antes de começar + +

    Uma lista de ferramentas, tecnologias que serão muito úteis para a construção de aplicativos com Frappe.

    + +Há uma série de bons tutoriais on-line e encontramos [Codecademy] (http://www.codecademy.com/) como um dos mais bonitos tutoriais, aqui há um monte de lições que você pode aprender com Codecademy + +--- + +#### 1. Python + +O lado do servidor do Frappe é escrito em Python e é uma boa idéia para [aprender rapidamente Python] (http://www.codecademy.com/tracks/python) antes de começar a se aprofundar em Frappe. Outro bom lugar para aprender Pytohn é o [tutorial no docs.python.org](https://docs.python.org/2.7/tutorial/index.html). Note-se que o Frappe usa Python 2.7 + +Para escrever código do lado do servidor de qualidade, você deve incluir testes automáticos. Você pode aprender as noções básicas de [test driven development (TDD) - aqui] (http://code.tutsplus.com/tutorials/beginning-test-driven-development-in-python--net-30137). + +--- + +#### 2. Databases MariaDB / MySQL + +Você precisa entender os conceitos básicos de bancos de dados, como instalar, login, criar novos bancos de dados e consultas SQL básicas. Aqui é um [introdução muito rápido para MySQL](https://www.digitalocean.com/community/tutorials/a-basic-mysql-tutorial) ou o [site do MariaDB para uma compreensão mais detalhada](https://mariadb.com/kb/en/mariadb/documentation/getting-started/) + +--- + +#### 3. HTML / CSS + +Se você está construindo interfaces de usuário, você precisará [conhecer o básico de HTML / CSS](http://www.codecademy.com/tracks/web) e o [Framework CSS - Boostrap](http://getbootstrap.com) + +--- + +#### 4. Building UI with Javascript and JQuery + +Para personalizar formulários e criar novas interfaces de usuário ricas, é melhor [saber JavaScript](http://www.codecademy.com/tracks/javascript) e a [biblioteca popular, JQuery](http://www.codecademy.com/faixas/jQuery). + +--- + +#### 5. Customizing Prints and Web pages with Jinja Templating + +Se estiver personalizando modelos de impressão, você precisa aprender a [línguagem Jinja Templating](http://jinja.pocoo.org/). É uma maneira fácil de criar páginas web dinâmicas (HTML). + +--- + +#### 6. Git and GitHub + +[Saiba como contribuir para um projeto open source usando Git e GitHub](https://guides.github.com/activities/contributing-to-open-source/), duas grandes ferramentas para ajudá-lo a gerir o seu código e compartilha-lo com os outros. + +--- + +Quando estiver pronto, [tente construir uma aplicação de exemplo em Frappe]({{docs_base_url}}/user/pt/tutorial/app) diff --git a/frappe/docs/user/pt/tutorial/bench.md b/frappe/docs/user/pt/tutorial/bench.md new file mode 100755 index 0000000000..5b764d7a8e --- /dev/null +++ b/frappe/docs/user/pt/tutorial/bench.md @@ -0,0 +1,10 @@ +# Instalando o Frappe Bench + +A maneira mais fácil de instalar o frappe em um sistema baseado em Unix, é utilizando o frappe-bench. Leia as instruções detalhadas sobre como instalar usando o Frappe Bench. + +> [https://github.com/frappe/bench](https://github.com/frappe/bench) + +Com Frappe Bench você será capaz de configurar e hospedar vários aplicativos e sites e vai também configurar um Virtualenv do Python de modo que você pode ter um ambiente isolado para executar seus aplicativos (e não terá conflito de versões com outros ambientes de desenvolvimento). + +O `bench` ferramenta de linha de comando também será instalado, o que irá ajudá-lo no desenvolvimento e na gestão da instalação. +{next} diff --git a/frappe/docs/user/pt/tutorial/conclusion.md b/frappe/docs/user/pt/tutorial/conclusion.md new file mode 100755 index 0000000000..8163004eeb --- /dev/null +++ b/frappe/docs/user/pt/tutorial/conclusion.md @@ -0,0 +1,6 @@ +# Conclusão + + +Esperamos que este tutorial lhe de uma visão geral de como as aplicações são desenvolvidas em Frappe. O objetivo era abordar brevemente os vários aspectos do desenvolvimento de aplicações e dar uma visão ampla. Para obter ajuda em questões específicas, olhe nossa API. + +Para obter ajuda, faça parte da comunidade no [canal de bate-papo no Gitter](https://gitter.im/frappe/erpnext) ou no [fórum de desenvolvedores](https://discuss.erpnext.com) diff --git a/frappe/docs/user/pt/tutorial/controllers.md b/frappe/docs/user/pt/tutorial/controllers.md new file mode 100755 index 0000000000..cf657e9d0d --- /dev/null +++ b/frappe/docs/user/pt/tutorial/controllers.md @@ -0,0 +1,59 @@ +# Controladores + +O passo seguinte seria a adição de métodos e handlers de eventos para os modelos. No aplicativo, devemos assegurar que se uma Library Transaction é feita, o artigo em questão deve estar em estoque e o membro que irá emprestar o artigo deve ter um filiação válida. + +Para isso, podemos escrever uma validação pouco antes do objeto de Library Transaction ser salvo. Para fazer isso, abra o template `library_management/doctype/library_transaction/library_transaction.py`. + +Este arquivo é o controlador para o objeto Library Transaction. Nele você pode escrever métodos para: + +1. `before_insert` +1. `validate` (antes de inserir ou atualizar) +1. `on_update` (depois de salvar) +1. `on_submit` (quando o documento é submetido) +1. `on_cancel` +1. `on_trash` (antes que ele esteja prestes a ser excluido) + +Você pode escrever métodos para esses eventos e eles serão chamados pelo framework quando o documento for salvo etc. + +Aqui é o controlador acabado: + + from __future__ import unicode_literals + import frappe + from frappe import _ + from frappe.model.document import Document + + class LibraryTransaction(Document): + def validate(self): + last_transaction = frappe.get_list("Library Transaction", + fields=["transaction_type", "transaction_date"], + filters = { + "article": self.article, + "transaction_date": ("<=", self.transaction_date), + "name": ("!=", self.name) + }) + if self.transaction_type=="Issue": + msg = _("Article {0} {1} has not been recorded as returned since {2}") + if last_transaction and last_transaction[0].transaction_type=="Issue": + frappe.throw(msg.format(self.article, self.article_name, + last_transaction[0].transaction_date)) + else: + if not last_transaction or last_transaction[0].transaction_type!="Issue": + frappe.throw(_("Cannot return article not issued")) + +Nesse script: + +1. Pegamos a última transação antes da data da transação atual usando a função de consulta `frappe.get_list` +1. Se a última transação for algo que não queremos, lançamos uma exceção usando `frappe.throw` +1. Usamos o método `_("text")` para identificar strings traduzíveis. + +Verifique se suas validações funcionaram, criando de novos registros. + +Transaction + +#### Debugging + +Para Debugar, mantenha sempre o seu Console JS aberto. procurando por erros de servidor e JavaScript. + +Além disso, verifique a sua janela do terminal para exceções. Quaisquer **500 Internal Server Errors** será impresso em seu terminal, onde o servidor está rodando. + +{next} diff --git a/frappe/docs/user/pt/tutorial/doctype-directory-structure.md b/frappe/docs/user/pt/tutorial/doctype-directory-structure.md new file mode 100755 index 0000000000..12b7a2f837 --- /dev/null +++ b/frappe/docs/user/pt/tutorial/doctype-directory-structure.md @@ -0,0 +1,31 @@ +# Estrutura de diretórios do DocType + +Depois de salvar os doctypes, verifique-se de que oo arquivos do modelo `.json` e `.py` foram criados no modulo `apps/library_management/library_management`. A estrutura de diretório após a criação dos modelos deve ficar assim: + + . + ├── MANIFEST.in + ├── README.md + ├── library_management + .. + │   ├── library_management + │   │   ├── __init__.py + │   │   └── doctype + │   │   ├── __init__.py + │   │   ├── article + │   │   │   ├── __init__.py + │   │   │   ├── article.json + │   │   │   └── article.py + │   │   ├── library_member + │   │   │   ├── __init__.py + │   │   │   ├── library_member.json + │   │   │   └── library_member.py + │   │   ├── library_membership + │   │   │   ├── __init__.py + │   │   │   ├── library_membership.json + │   │   │   └── library_membership.py + │   │   └── library_transaction + │   │   ├── __init__.py + │   │   ├── library_transaction.json + │   │   └── library_transaction.py + +{next} diff --git a/frappe/docs/user/pt/tutorial/doctypes.md b/frappe/docs/user/pt/tutorial/doctypes.md new file mode 100755 index 0000000000..fa6c7a26ee --- /dev/null +++ b/frappe/docs/user/pt/tutorial/doctypes.md @@ -0,0 +1,95 @@ +# DocType + +Depois de criar as Roles, vamos criar os **DocTypes** + +Para criar um novo **DocType**, vá para: + +> Developer > Documents > Doctype > New + +New Doctype + +No DocType, criamos o módulo, que no nosso caso é **Library Managment** + +#### Adicionando Campos + +Na Tabela, você pode adicionar os campos (fields) do DocType (Article). + +Os campos são muito mais do que colunas de banco de dados, eles podem ser: + +1. Colunas no banco de dados +1. Layout helpers (Seção / quebras de coluna) +1. Tabelas filho (Tabela como tipo de uma propriedade) +1. HTML +1. Ações (botões) +1. Anexos ou imagens + +Vamos adicionar os campos do artigo. + +Adding Fields + +Quando você adiciona campos, você precisa digitar o **Type**. **Label** é opcional para quebra de seção e quebra de coluna. **Name** (`fieldname`) é o nome da coluna da tabela de banco de dados e também a propriedade do controlador. Isso tem que ser um *código amigável*, ou seja, ele tem que ter caracteres minusculos e _ em vez de "". Se você deixar o nome do campo em branco, ele será ajustado automaticamente quando você salvá-lo. + +Você também pode definir outras propriedades do campo como se é obrigatório, apenas para leitura etc. + +Nós podemos adicionar os seguintes campos: + +1. Article Name (Data) +2. Author (Data) +3. Description +4. ISBN +5. Status (Select): Para Selecionar campos, você vai entrar nas opções. Digite **Issued** e **Available** cada um em uma nova linha na caixa de Opções. Consulte o diagrama abaixo +6. Publisher (Data) +7. Language (Data) +8. Image (Attach Image) + + +#### Adicionando permissões + +Depois de adicionar os campos, finalize e adicione uma nova linha na seção Regras de permissão. Por enquanto, vamos dar permissão de Read, Write, Create, Delete and Report, a **Librarian**. Frappe tem uma Role baseado nas permissões do modelo. Você também pode alterar as permissões posteriormente usando o **Role Permissions Manager** do **Setup**. + +Adding Permissions + +#### Salvando + +Click no botão **Save**. Quando o botão for clicado, um popup irá te pedir um nome. De o nome de **Article** e salve o DocType. + +Agora logue no mysql e verifique se a tabela do banco de dados foi criada: + + $ bench mysql + Welcome to the MariaDB monitor. Commands end with ; or \g. + Your MariaDB connection id is 3931 + Server version: 5.5.36-MariaDB-log Homebrew + + Copyright (c) 2000, 2014, Oracle, Monty Program Ab and others. + + Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. + + MariaDB [library]> DESC tabArticle; + +--------------+--------------+------+-----+---------+-------+ + | Field | Type | Null | Key | Default | Extra | + +--------------+--------------+------+-----+---------+-------+ + | name | varchar(255) | NO | PRI | NULL | | + | creation | datetime(6) | YES | | NULL | | + | modified | datetime(6) | YES | | NULL | | + | modified_by | varchar(40) | YES | | NULL | | + | owner | varchar(60) | YES | | NULL | | + | docstatus | int(1) | YES | | 0 | | + | parent | varchar(255) | YES | MUL | NULL | | + | parentfield | varchar(255) | YES | | NULL | | + | parenttype | varchar(255) | YES | | NULL | | + | idx | int(8) | YES | | NULL | | + | article_name | varchar(255) | YES | | NULL | | + | status | varchar(255) | YES | | NULL | | + | description | text | YES | | NULL | | + | image | varchar(255) | YES | | NULL | | + | publisher | varchar(255) | YES | | NULL | | + | isbn | varchar(255) | YES | | NULL | | + | language | varchar(255) | YES | | NULL | | + | author | varchar(255) | YES | | NULL | | + +--------------+--------------+------+-----+---------+-------+ + 18 rows in set (0.00 sec) + + +Como você pode ver, junto com os DocFields, várias colunas padrão também foram adicionados à tabela. Importante notar aqui que, a chave primária, `name`,` owner` (o usuário que criou o registro), `creation` e` modified` (timestamps para a criação e última modificação). + +{next} diff --git a/frappe/docs/user/pt/tutorial/form-client-scripting.md b/frappe/docs/user/pt/tutorial/form-client-scripting.md new file mode 100755 index 0000000000..c37196757f --- /dev/null +++ b/frappe/docs/user/pt/tutorial/form-client-scripting.md @@ -0,0 +1,37 @@ +## Escrevendo script de formulários + +Até agora nós criamos um sistema básico que funciona fora da caixa, sem ter que escrever nenhum código. Vamos agora escrever alguns scripts para tornar a aplicação mais rica e adicionar validações de formulários para o usuário não inserir dados incorretos. + +### Script no Lado do Cliente + +No DocType **Library Transaction**, temos um único campo de nome do membro. Não fizemos dois campos. Agora, isso poderia muito bem ser dois campos (e provavelmente deve), mas por uma questão de exemplo, vamos considerar que temos que implementar isto. Para fazer isso, teria que escrever um Handler de eventos para um evento para quando o usuário selecionar o campo `library_member`, acessar o recurso membro do servidor usando REST API e inserir os valores no formulário. + +Para iniciar o script, na pasta `library_management/doctype/library_transaction`, crie um novo arquivo `library_transaction.js`. Este arquivo será executado automaticamente quando a primeiro Library Transaction for aberta pelo usuário. Portanto, neste arquivo, podemos ligar os eventos e escrever outras funções. + +#### library_transaction.js + + frappe.ui.form.on("Library Transaction", "library_member", + function(frm) { + frappe.call({ + "method": "frappe.client.get", + args: { + doctype: "Library Member", + name: frm.doc.library_member + }, + callback: function (data) { + frappe.model.set_value(frm.doctype, + frm.docname, "member_name", + data.message.first_name + + (data.message.last_name ? + (" " + data.message.last_name) : "")) + } + }) + }); + +1. **frappe.ui.form.on(*doctype*, *fieldname*, *handler*)** é utilizado para ligar um handler ao evento quando a propriedade library_member for definida. +1. No handler, nós desencadear uma chamada AJAX para `frappe.client.get`. Em resposta obtemos o objeto solicitado como JSON. [Saiba mais sobre a API](/frappe/user/en/guides/integration/rest_api). +1. Utilizando **frappe.model.set_value(*doctype*, *name*, *fieldname*, *value*)** Nós inserimos o valor no formulário. + +**Observação:** Para verificar se o script funciona, lembre-se de 'recarregar' a página antes de testar seu script. mudanças no script do cliente não são captadas automaticamente quando você está no modo de desenvolvedor.. + +{next} diff --git a/frappe/docs/user/pt/tutorial/index.md b/frappe/docs/user/pt/tutorial/index.md new file mode 100755 index 0000000000..56f5817fc5 --- /dev/null +++ b/frappe/docs/user/pt/tutorial/index.md @@ -0,0 +1,33 @@ +# Frappe Tutorial + +In this guide we will show you how to create an application from scratch using **Frappe**. Using the example of a Library Management System, we will cover: + +1. Installation +1. Making a New App +1. Making Models +1. Creating Users and Records +1. Creating Controllers +1. Creating Web Views +1. Setting Hooks and Tasks + +## Who is This For? + +This guide is intended for software developers who are familiar with how the web applications are built and served. Frappe Framework is built on Python and uses MariaDB database and for creating web views, HTML/CSS/Javascript is used. So it would be great if you are familiar with all these technologies. At minimum if you have never used Python before, you should take a quick tutorial before your use this Guide. + +Frappe uses the git version control system on GitHub. It is also important that you are familiar with basic git and have an account on GitHub to manage your applications. + +## Example + +For this guide book, we will build a simple **Library Management** application. In this application we will have models: + +1. Article (Book or any other item that can be loaned) +1. Library Member +1. Library Transaction (Issue or Return of an article) +1. Library Membership (A period in which a member is allowed to transact) +1. Library Management Setting (Global settings like period of loan) + +The user interface (UI) for the librarian will be the **Frappe Desk**, a built-in browser based UI environment where forms are automatically generated from the models and roles and permissions are also applied. + +We will also create web views for library where users can browse articles from a website. + +{index} diff --git a/frappe/docs/user/pt/tutorial/index.txt b/frappe/docs/user/pt/tutorial/index.txt new file mode 100755 index 0000000000..1fed6aed93 --- /dev/null +++ b/frappe/docs/user/pt/tutorial/index.txt @@ -0,0 +1,19 @@ +before +app +bench +new-app +setting-up-the-site +start +models +roles +doctypes +naming-and-linking +doctype-directory-structure +users-and-records +form-client-scripting +controllers +reports +web-views +single-doctypes +task-runner +conclusion diff --git a/frappe/docs/user/pt/tutorial/models.md b/frappe/docs/user/pt/tutorial/models.md new file mode 100755 index 0000000000..f31c5c9864 --- /dev/null +++ b/frappe/docs/user/pt/tutorial/models.md @@ -0,0 +1,19 @@ +# Criando modelos + +O próximo passo é criar os modelos como discutimos na introdução. Em Frappe, os modelos são chamados **DocTypes**. Você pode criar novos doctypes atravez da interface do Desk. **DocTypes** são feitos de campos chamados **DocField** e de permissões com base nas permissões que são integrados nos modelos, estes são chamados **DocPerms**. + +Quando um DocType é salvo, uma nova tabela é criada no banco de dados. Esta tabela é nomeado como `tab[doctype]`. + +Quando você cria um **DocType** uma nova pasta é criada no **Módulo** e um arquivo JSON do modelo e um controlador template em Python são criados automaticamente. Quando você atualizar o DocType, o arquivo modelo JSON é atualizado e quando o `bench migrate` é executado, ele é sincronizado com o banco de dados. Isto torna mais fácil para propagar alterações de schema e migrar. + +### Modo Desenvolvedor + +Para criar modelos, você deve definir `developer_mode` como 1 no arquivo `site_config.json` localizado em /sites/library e executar o comando `bench clear-cache` ou use o menu de usuário na interface do usuário e clique em "Atualizar" para que as alterações entrem em vigor. Agora você deve ver o aplicativo "Developer" em sua Desk + + { + "db_name": "bcad64afbf", + "db_password": "v3qHDeVKvWVi7s97", + "developer_mode": 1 + } + +{next} diff --git a/frappe/docs/user/pt/tutorial/naming-and-linking.md b/frappe/docs/user/pt/tutorial/naming-and-linking.md new file mode 100755 index 0000000000..3a36e9f457 --- /dev/null +++ b/frappe/docs/user/pt/tutorial/naming-and-linking.md @@ -0,0 +1,71 @@ +# Nomeando e vinculando DocType + +Em seguida, vamos criar outro DocType e salva-lo também: + +1. Library Member (First Name, Last Name, Email ID, Phone, Address) + +Doctype Saved + + +#### Nomeação de DocTypes + +DocTypes podem ser nomeados de diferentes formas: + +1. Com base em um campo +1. Com base numa série +1. Pelo controlador (Código) +1. Prompt + +Isso pode ser definido através do preenchimento do campo **Autoname**. Para o controlador, deixe em branco. + +> **Search Fields**: A DocType pode ser nomeado em uma série, mas ele ainda precisa ser pesquisado por nome. No nosso caso, o artigo será procurado pelo título ou o nome do autor. Portanto, este pode ser inserido no campo de pesquisa. + +Autonaming and Search Field + +#### Vinculando e selecionando campos + +As chaves estrangeiras são especificados no Frappe como um tipo de campo **Link**. O DocType alvo deve ser mencionado na área de Opções de texto. + +No nosso exemplo, na Library Transaction DocType, temos que ligar o Membro da Biblioteca e o artigo. + +**Observação:** Lembre-se que os campos link não são automaticamente configurados como chaves estrangeiras no banco de dados MariaDB, porque isso vai implicitamente indexar a coluna. Isto pode não ser ideal, mas, a validação de chave estrangeira é feito pelo Framework. + +Link Field + +Para campos de multipla escolha, como mencionamos anteriormente, adicione as várias opções na caixa de entrada **Options**, cada opção em uma nova linha. + +Select Field + +Fazer o mesmo para outros modelos. + +#### Vinculando valores + +Um modelo padrão é quando você seleciona um ID, **Library Member** na **Library Membership**, então, o primeiro e o ultimo nome dos membros devem ser copiados para os campos adequados ao gravar na Library Membership Transaction. + +Para fazer isso, podemos usar campos de somente leitura e de opções, podemos definir o nome do link e o nome do campo da propriedade que deseja buscar. Para este exemplo no **Member First Name** podemos definir `library_member.first_name` + +Fetch values + +### Complete os modelos + +Da mesma forma, você pode completar todos os modelos de modo que os campos finais fiquem parecido com este: + +#### Article + +Article + +#### Library Member + +Library Member + +#### Library Membership + +Library Membership + +#### Library Transaction + +Library Transaction + +> Lembre-se de dar permissões para **Librarian** em cada DocType + +{next} diff --git a/frappe/docs/user/pt/tutorial/new-app.md b/frappe/docs/user/pt/tutorial/new-app.md new file mode 100755 index 0000000000..1390f60766 --- /dev/null +++ b/frappe/docs/user/pt/tutorial/new-app.md @@ -0,0 +1,54 @@ +# Crie um novo aplicativo + +Uma vez que o banco ja estiver instalado, você verá duas pastas principais, `apps` e` sites`. Todos os aplicativos serão instalados em apps. + +Para criar uma nova aplicação, vá para a pasta do bench e execute, `bench new-app {app_name}` e preencha os detalhes sobre o aplicativo. Isto irá criar uma aplicação base para você. + + $ bench new-app library_management + App Title (defaut: Lib Mgt): Library Management + App Description: App for managing Articles, Members, Memberships and Transactions for Libraries + App Publisher: Frappe + App Email: info@frappe.io + App Icon (default 'octicon octicon-file-directory'): octicon octicon-book + App Color (default 'grey'): #589494 + App License (default 'MIT'): GNU General Public License + +### Estrutura do aplicativo + +O aplicativo será criado em uma pasta chamada `library_management` e terá a seguinte estrutura:: + + . + ├── MANIFEST.in + ├── README.md + ├── library_management + │   ├── __init__.py + │   ├── config + │   │   ├── __init__.py + │   │   └── desktop.py + │   ├── hooks.py + │   ├── library_management + │   │   └── __init__.py + │   ├── modules.txt + │   ├── patches.txt + │   └── templates + │   ├── __init__.py + │   ├── generators + │   │   └── __init__.py + │   ├── pages + │   │   └── __init__.py + │   └── statics + ├── license.txt + ├── requirements.txt + └── setup.py + +1. `config` pasta que contém as informações de configuração do aplicativo +1. `desktop.py` é onde os ícones da área de trabalho pode ser adicionado ao Desk +1. `hooks.py` é onde integrações com o ambiente da aplicação e outras aplicações é mencionada. +1. `library_management` (Interior) é um **módulo** que foi criado. Em Frappe, um **módulo** é onde os arquivos do modelo e do controlador residem. +1. `modules.txt` contém a lista dos **módulos** do aplicativo. Quando você cria um novo módulo, é necessário que você atualize este arquivo. +1. `patches.txt` é o lugar onde os patches de migração são escritos. Eles são referências de módulos Python utilizando a notação de ponto. +1. `templates` é a pasta onde os modelos de web view são mantidos. Modelos para **Login** e outras páginas padrão são criadas pelo frappe. +1. `generators` é onde os templates para os modelos são mantidas, onde cada instância de modelo tem uma rota web separada, por exemplo, um **Post de um Blog**, onde cada post tem a sua única url web. Em Frappe, o mecanismo de modelagem utilizada é o Jinja2 +1. `pages` É onde uma única rota para os modelos são mantidas. Por exemplo, para um "/blog" tipo da página. + +{next} diff --git a/frappe/docs/user/pt/tutorial/reports.md b/frappe/docs/user/pt/tutorial/reports.md new file mode 100755 index 0000000000..1889705874 --- /dev/null +++ b/frappe/docs/user/pt/tutorial/reports.md @@ -0,0 +1,7 @@ +# Relatórios + +Você também pode clicar sobre o texto Relatórios na barra lateral (esquerda) para ver os registros tabulados + +Report + +{next} diff --git a/frappe/docs/user/pt/tutorial/roles.md b/frappe/docs/user/pt/tutorial/roles.md new file mode 100755 index 0000000000..286d39afa7 --- /dev/null +++ b/frappe/docs/user/pt/tutorial/roles.md @@ -0,0 +1,14 @@ +# Criando Roles + +Antes de criar modelos, devemos criar roles para que possamos definir as permissões do modelo. Há dois roles que nós iremos criar: + +1. Librarian +1. Library Member + +Para criar um novo roles, vá para: + +> Setup > Users > Role > New + +Adding Roles + +{next} diff --git a/frappe/docs/user/pt/tutorial/setting-up-the-site.md b/frappe/docs/user/pt/tutorial/setting-up-the-site.md new file mode 100755 index 0000000000..5ab07fa01b --- /dev/null +++ b/frappe/docs/user/pt/tutorial/setting-up-the-site.md @@ -0,0 +1,53 @@ +# Configurando o site + +Vamos criar um novo site e chamá-lo de `library`. + +Você pode instalar um novo site, pelo comando `bench new-site library` + +Isto irá criar uma nova pasta para o site e um banco de dados e instalar o `frappe` (que também é uma aplicação!) No novo site. A aplicação `frappe` tem dois módulos embutidos **Core** e **WebSite**. O módulo de Core contém os modelos básicos para a aplicação. Frappe é uma estrutura como as pilhas e vem com um monte de modelos internos. Estes modelos são chamados doctypes **Mais sobre isso mais tarde**. + + $ bench new-site library + MySQL root password: + Installing frappe... + Updating frappe : [========================================] + Updating country info : [========================================] + Set Administrator password: + Re-enter Administrator password: + Installing fixtures... + *** Scheduler is disabled *** + +### Estrututa do Site + +Uma nova pasta chamada `library` será criado na pasta` sites`. Aqui está a estrutura de pastas padrão para um site. + + . + ├── locks + ├── private + │   └── backups + ├── public + │   └── files + └── site_config.json + +1. `public/files` é onde os arquivos enviados pelo usuário são armazenados. +1. `private/backups` é onde os backups são despejados +1. `site_config.json` é onde as configurações a nível do site são mantidas. + +### Configurações padrão do site + +No caso de você ter vários sites em seu bench use `bench use [site_name]` para definir o site padrão. + +Exemplo: + + $ bench use library + +### Instalar App + +Agora vamos instalar nosso app `library_management` no nosso site `library` + +1. Instale library_management no library com: `bench --site [site_name] install-app [app_name]` + +Exemplo: + + $ bench --site library install-app library_management + +{next} diff --git a/frappe/docs/user/pt/tutorial/single-doctypes.md b/frappe/docs/user/pt/tutorial/single-doctypes.md new file mode 100755 index 0000000000..c21115244e --- /dev/null +++ b/frappe/docs/user/pt/tutorial/single-doctypes.md @@ -0,0 +1,9 @@ +# Single DocTypes + +A aplicação irá normalmente têm uma página de configurações. Em nossa aplicação, podemos definir uma página onde podemos definir o período de empréstimo. Também precisamos salvar esta propriedade. Em Frappe, isso pode ser feito usando um tipo DocType **Single** . Um DocType Single é como o padrão Singleton em Java. É um objecto com uma única instância. Vamos chamar isso de **Library Managment Settings**. + +Para criar um DocType Single, marque a propriedade **Is Single** como verdadeira. + +Single Doctypes + +{next} diff --git a/frappe/docs/user/pt/tutorial/start.md b/frappe/docs/user/pt/tutorial/start.md new file mode 100755 index 0000000000..c4ed5e5f3c --- /dev/null +++ b/frappe/docs/user/pt/tutorial/start.md @@ -0,0 +1,31 @@ +# Iniciando o Bench + +Agora podemos logar e verificar se tudo funcionou. + +Para iniciar o servidor de desenvolvimento, digite `bench start` + + $ bench start + 13:58:51 web.1 | started with pid 22135 + 13:58:51 worker.1 | started with pid 22136 + 13:58:51 workerbeat.1 | started with pid 22137 + 13:58:52 web.1 | * Running on http://0.0.0.0:8000/ + 13:58:52 web.1 | * Restarting with reloader + 13:58:52 workerbeat.1 | [2014-09-17 13:58:52,343: INFO/MainProcess] beat: Starting... + +Agora você pode abrir o seu navegador e ir para `http://localhost:8000`. Você deve ver esta página de login, se tudo correu bem: + +Login Screen + +Agora logue com : + +Login ID: **Administrator** + +Senha : **Use a senha que foi criada durante a instalação** + +Quando voce logar, voce deverá ver o "Desk" da pagine home + +Desk + +Como você pode ver, o básico do sistema Frappe vem com vários aplicativos pré-carregados como coisas a fazer, o Gerenciador de arquivos etc. Esses aplicativos podem ser integrados no fluxo de trabalho do app à medida que progredimos. + +{next} diff --git a/frappe/docs/user/pt/tutorial/task-runner.md b/frappe/docs/user/pt/tutorial/task-runner.md new file mode 100755 index 0000000000..c0f696410f --- /dev/null +++ b/frappe/docs/user/pt/tutorial/task-runner.md @@ -0,0 +1,77 @@ +# Tarefas agendadas + +Finalmente, uma aplicação também tem que enviar notificações de e-mail e fazer outros tipos de tarefas agendadas. Em Frappe, se você instalou o bench, o task / scheduler foi instalado via Celery usando Redis Queue. + +Para adicionar um novo task handler, vá para `hooks.py` e adicione um novo handler. Handlers padrão são os `all`,` daily`, `weekly`,` monthly`. O handler `all` é chamado a cada 3 minutos por padrão. + + # Scheduled Tasks + # --------------- + + scheduler_events = { + "daily": [ + "library_management.tasks.daily" + ], + } + +Aqui podemos apontar para uma função Python e esta função será executada todos os dias. Vejamos como é essa função: + + # Copyright (c) 2013, Frappe + # For license information, please see license.txt + + from __future__ import unicode_literals + import frappe + from frappe.utils import datediff, nowdate, format_date, add_days + + def daily(): + loan_period = frappe.db.get_value("Library Management Settings", + None, "loan_period") + + overdue = get_overdue(loan_period) + + for member, items in overdue.iteritems(): + content = """

    Following Items are Overdue

    +

    Please return them as soon as possible

      """ + + for i in items: + content += "
    1. {0} ({1}) due on {2}
    2. ".format(i.article_name, + i.article, + format_date(add_days(i.transaction_date, loan_period))) + + content += "
    " + + recipient = frappe.db.get_value("Library Member", member, "email_id") + frappe.sendmail(recipients=[recipient], + sender="test@example.com", + subject="Library Articles Overdue", content=content, bulk=True) + + def get_overdue(loan_period): + # check for overdue articles + today = nowdate() + + overdue_by_member = {} + articles_transacted = [] + + for d in frappe.db.sql("""select name, article, article_name, + library_member, member_name + from `tabLibrary Transaction` + order by transaction_date desc, modified desc""", as_dict=1): + + if d.article in articles_transacted: + continue + + if d.transaction_type=="Issue" and \ + datediff(today, d.transaction_date) > loan_period: + overdue_by_member.setdefault(d.library_member, []) + overdue_by_member[d.library_member].append(d) + + articles_transacted.append(d.article) + +Nós podemos colocar o código acima em qualquer módulo Python acessível. A rota é definida em `hooks.py`, portanto, para os nossos propósitos, iremos colocar esse código em `library_management/tasks.py`. + +Observação: + +1. Nós pegamos o loan period de **Library Management Settings** usando `frappe.db.get_value`. +1. Nós rodamos uma query no banco de dados com `frappe.db.sql` +1. O email foi enviado via `frappe.sendmail` + +{next} diff --git a/frappe/docs/user/pt/tutorial/users-and-records.md b/frappe/docs/user/pt/tutorial/users-and-records.md new file mode 100755 index 0000000000..878253bf0b --- /dev/null +++ b/frappe/docs/user/pt/tutorial/users-and-records.md @@ -0,0 +1,55 @@ +# Fazendo Usuários e Registros + +Agora que já criamos os modelos, podemos começar diretamente criando registros usando a interface Desk do Frappe. Você não precisa criar Views! Views no Frappe são geradas automaticamente com base nas propriedades do DocType. + +### 4.1 Criando Usuarios + +Para criar registros, vamos primeiro criar um usuário. Para criar um usuário, vá para: + +> Setup > Users > User > New + +Crie um novo usuário e definá o nome, o primeiro nome e uma nova senha. + +Também de as roles de Librarian e Library Member para este usuario + +Add User Roles + +Agora saia e se autentique usando o novo ID de usuário e senha. + +### 4.2 Criando registros + +Você vai ver agora um ícone para o módulo de Library Management. Clique nesse ícone e você verá a página do modelo: + +Library Management Module + +Aqui você pode ver os doctypes que criamos para a aplicação. Vamos começar a criar alguns registros. + +Primeiro, vamos criar um novo artigo: + +New Article + +Aqui você vai ver que o DocType que você tinha criado foi processado como um formulário. As validações e outras regras também serão aplicadas conforme projetado. Vamos preencher um artigo. + +New Article + +Você também pode adicionar uma imagem. + +Attach Image + +Agora vamos criar um novo membro: + +New Library Member + +Depois disso, vamos criar um novo registro de membership para o membro. + +Aqui se você se lembra, nós tinhamos definido os valores do primeiro e do ultimo nome do membro para ser diretamente obtido a partir dos registros de membros e, logo que você selecionar o ID de membro, os nomes serão atualizados. + +New Library Membership + +Como você pode ver que a data é formatada como ano-mês-dia, que é um formato de sistema. Para definir/mudar a data, hora e número de formatos, acesse + +> Setup > Settings > System Settings + +System Settings + +{next} diff --git a/frappe/docs/user/pt/tutorial/web-views.md b/frappe/docs/user/pt/tutorial/web-views.md new file mode 100755 index 0000000000..4b9dfa29bf --- /dev/null +++ b/frappe/docs/user/pt/tutorial/web-views.md @@ -0,0 +1,64 @@ +# Web Views + +Frappe tem dois ambientes de usuário principais, o Desk e o Web. Desk é um ambiente UI controlado com uma rica aplicação AJAX e a web usa template HTML tradicional que serve para consumo público. Web Views também podem ser gerados para criar views mais controladas para os usuários que pode fazer o login mas ainda não têm acesso à Desk. + +Em Frappe, Web Views são geridas por modelos e eles geralmente estão na pasta `templates`. Existem 2 tipos principais de templates. + +1. Pages: Estes são Jinja Templates, onde existe uma única view para uma única rota web, por exemplo, `/blog`. +2. Generators: Estes são templates em que cada instância de um DocType tem uma rota web separada `/blog/a-blog`, `blog/b-blog` etc. +3. Lists and Views: Estas são listas e views padrões com a rota `[doctype]/[name]` e são processadas com base na permissão. + +### Standard Web Views + +> Esta funcionalidade ainda esta em desenvolvimento. + +Vamos dar uma olhada na standard Web Views: + +Se você estiver logado como usuário de teste, vá para `/article` e você deverá ver a lista de artigos: + +![Web List]({{docs_base_url}}/assets/img/guide/26-web-list.png) + +Clique em um artigo e você vai ver uma Web View padrão + +![Web List]({{docs_base_url}}/assets/img/guide/26-web-view.png) + +Agora, se você quiser fazer uma List View melhor para o artigo, crie um arquivo chamado `list_item.html` na pasta `library_management/doctype/article`. Aqui está um exemplo de arquivo: + + {% raw %}
    +
    + + + +
    +
    +

    {{ doc.article_name }}

    +

    {{ doc.author }}

    +

    {{ (doc.description[:200] + "...") + if doc.description|len > 200 else doc.description }}

    +

    Publisher: {{ doc.publisher }}

    +
    +
    {% endraw %} + + +Aqui, você vai ter todas as propriedades do artigo no objeto `doc`. + +A List View atualizada se parece com isso! + +![Web List]({{docs_base_url}}/assets/img/guide/27-web-view-list.png) + +#### Home Page + +Frappe também tem um fluxo de trabalho de inscrição built-in que também inclui inscrições de terceiros via Google, Facebook e GitHub. Quando um usuário se inscreve na web, ele não tem acesso à interface Desk por padrão. + +> Para permitir o acesso do usuário ao Desk, abra as configurações pelo Setup > User e defina o usuário como "System User" + +Agora, para os não usuários do sistema, podemos definir uma home page para quando eles fizerem login via `hooks.py` com baseado na Role. + +Para quando os membros da biblioteca entrarem, eles devem ser redirecionado para a página `article`, para abrir o arquivo `library_management/hooks.py` adicione: + + role_home_page = { + "Library Member": "article" + } + +{next} diff --git a/frappe/docs/user/fr/videos/__init__.py b/frappe/email/doctype/auto_email_report/__init__.py similarity index 100% rename from frappe/docs/user/fr/videos/__init__.py rename to frappe/email/doctype/auto_email_report/__init__.py diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.js b/frappe/email/doctype/auto_email_report/auto_email_report.js new file mode 100644 index 0000000000..f01fec1c76 --- /dev/null +++ b/frappe/email/doctype/auto_email_report/auto_email_report.js @@ -0,0 +1,90 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Auto Email Report', { + refresh: function(frm) { + if(frm.doc.report_type !== 'Report Builder') { + if(frm.script_setup_for !== frm.doc.report) { + frappe.call({ + method:"frappe.desk.query_report.get_script", + args: { + report_name: frm.doc.report + }, + callback: function(r) { + frappe.dom.eval(r.message.script || ""); + frm.script_setup_for = frm.doc.report; + frm.trigger('show_filters'); + } + }); + } else { + frm.trigger('show_filters'); + } + } + if(!frm.is_new()) { + frm.add_custom_button(__('Download'), function() { + var w = window.open( + frappe.urllib.get_full_url( + "/api/method/frappe.email.doctype.auto_email_report.auto_email_report.download?" + +"name="+encodeURIComponent(frm.doc.name))); + if(!w) { + msgprint(__("Please enable pop-ups")); return; + } + }); + frm.add_custom_button(__('Send Now'), function() { + frappe.call({ + method: 'frappe.email.doctype.auto_email_report.auto_email_report.send_now', + args: {name: frm.doc.name}, + callback: function() { + msgprint(__('Scheduled to send')); + } + }); + }); + } else { + if(!frm.doc.user) { + frm.set_value('user', frappe.session.user); + } + if(!frm.doc.email_to) { + frm.set_value('email_to', frappe.session.user); + } + } + }, + show_filters: function(frm) { + var wrapper = $(frm.get_field('filters_display').wrapper); + wrapper.empty(); + if(frm.doc.report_type !== 'Report Builder' + && frappe.query_reports[frm.doc.report] + && frappe.query_reports[frm.doc.report].filters) { + + // make a table to show filters + var table = $('\ + \ +
    '+__('Filter')+''+__('Value')+'
    ').appendTo(wrapper); + $('

    ' + __("Click table to edit") + '

    ').appendTo(wrapper); + var filters = JSON.parse(frm.doc.filters || '{}'); + var report_filters = frappe.query_reports[frm.doc.report].filters; + + report_filters.forEach(function(f) { + $('' + f.label + ''+ frappe.format(filters[f.fieldname], f) +'') + .appendTo(table.find('tbody')); + }); + + table.on('click', function() { + var dialog = new frappe.ui.Dialog({ + fields: report_filters, + primary_action: function() { + var values = this.get_values(); + if(values) { + this.hide(); + frm.set_value('filters', JSON.stringify(values)); + frm.trigger('show_filters'); + frappe.query_report_filters_by_name = null; + } + } + }); + dialog.show(); + dialog.set_values(filters); + frappe.query_report_filters_by_name = dialog.fields_dict; + }) + } + } +}); diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.json b/frappe/email/doctype/auto_email_report/auto_email_report.json new file mode 100644 index 0000000000..2c8ecf191e --- /dev/null +++ b/frappe/email/doctype/auto_email_report/auto_email_report.json @@ -0,0 +1,559 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 1, + "autoname": "", + "beta": 0, + "creation": "2016-09-01 01:34:34.985457", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "report", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Report", + "length": 0, + "no_copy": 0, + "options": "Report", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "User", + "fieldname": "user", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Based on Permissions For User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Enabled", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "1", + "fieldname": "send_if_data", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Send only if there is any data", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_4", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "report_type", + "fieldtype": "Read Only", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Report Type", + "length": 0, + "no_copy": 0, + "options": "report.report_type", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "100", + "description": "", + "fieldname": "no_of_rows", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "No of Rows (Max 500)", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.report_type !== 'Report Builder'", + "fieldname": "report_filters", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Report Filters", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "filters_display", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Filters Display", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "filters", + "fieldtype": "Text", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Filters", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "email_settings", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Email Settings", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "email_to", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Email To", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Monday", + "depends_on": "eval:doc.frequency=='Weekly'", + "fieldname": "day_of_week", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Day of Week", + "length": 0, + "no_copy": 0, + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_13", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "frequency", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Frequency", + "length": 0, + "no_copy": 0, + "options": "Daily\nWeekly\nMonthly", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "format", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Format", + "length": 0, + "no_copy": 0, + "options": "XLS\nHTML\nCSV", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "section_break_15", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Message", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "description", + "fieldtype": "Text Editor", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Message", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2016-09-23 01:06:56.963190", + "modified_by": "Administrator", + "module": "Email", + "name": "Auto Email Report", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Report Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py new file mode 100644 index 0000000000..3a764bf1c1 --- /dev/null +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.model.document import Document +import frappe.utils +from frappe.utils.xlsutils import get_xls +from frappe.utils.csvutils import to_csv + +max_reports_per_user = 3 + +class AutoEmailReport(Document): + def autoname(self): + self.name = _(self.report) + + def validate(self): + self.validate_report_count() + self.validate_emails() + + def validate_emails(self): + '''Cleanup list of emails''' + if ',' in self.email_to: + self.email_to.replace(',', '\n') + + valid = [] + for email in self.email_to.split(): + if email: + frappe.utils.validate_email_add(email, True) + valid.append(email) + + self.email_to = '\n'.join(valid) + + def validate_report_count(self): + '''check that there are only 3 enabled reports per user''' + count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0] + if count > max_reports_per_user: + frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user)) + + def get_report_content(self): + '''Returns file in for the report in given format''' + report = frappe.get_doc('Report', self.report) + raw = report.get_data(limit=self.no_of_rows or 100, user = self.user, filters = self.filters) + + if len(raw)==1 and self.send_if_data: + return None + + if self.format == 'HTML': + return self.get_html_table(raw) + + elif self.format == 'XLS': + return get_xls(raw) + + elif self.format == 'CSV': + return to_csv(raw) + + else: + frappe.throw(_('Invalid Output Format')) + + def get_html_table(self, data): + return frappe.render_template('frappe/templates/includes/print_table.html', { + 'headings': data[0], + 'data': data[1:] + }) + + def get_file_name(self): + return "{0}.{1}".format(self.report.replace(" ", "-").replace("/", "-"), self.format.lower()) + + def send(self): + data = self.get_report_content() + if not data: + return + + attachments = None + message = '

    {0}

    '.format(_('{0} generated on {1}')\ + .format(frappe.bold(self.name), + frappe.utils.format_datetime(frappe.utils.now_datetime()))) + + if self.description: + message += '
    ' + self.description + + if self.format=='HTML': + message += '
    ' + data + else: + attachments = [{ + 'fname': self.get_file_name(), + 'fcontent': data + }] + + message += '

    Edit Auto Email Report Settings: {0}

    '.format(frappe.utils.get_link_to_form('Auto Email Report', self.name)) + + frappe.sendmail( + recipients = self.email_to.split(), + subject = self.name, + message = message, + attachments = attachments + ) + +@frappe.whitelist() +def download(name): + '''Download report locally''' + auto_email_report = frappe.get_doc('Auto Email Report', name) + auto_email_report.check_permission() + data = auto_email_report.get_report_content() + + if not data: + frappe.msgprint(_('No Data')) + return + + frappe.local.response.filecontent = data + frappe.local.response.type = "download" + frappe.local.response.filename = auto_email_report.get_file_name() + +@frappe.whitelist() +def send_now(name): + '''Send Auto Email report now''' + auto_email_report = frappe.get_doc('Auto Email Report', name) + auto_email_report.check_permission() + auto_email_report.send() + +def send_daily(): + '''Check reports to be sent daily''' + now = frappe.utils.now_datetime() + for report in frappe.get_all('Auto Email Report', + {'enabled': 1, 'frequency': ('in', ('Daily', 'Weekly'))}): + auto_email_report = frappe.get_doc('Auto Email Report', report.name) + + # if not correct weekday, skip + if auto_email_report.frequency=='Weekly': + if now.weekday()!={'Monday':0,'Tuesday':1,'Wednesday':2, + 'Thursday':3,'Friday':4,'Saturday':5,'Sunday':6}[auto_email_report.weekday]: + continue + + auto_email_report.send() + + +def send_monthly(): + '''Check reports to be sent monthly''' + for report in frappe.get_all('Auto Email Report', {'enabled': 1, 'frequency': 'Monthly'}): + frappe.get_doc('Auto Email Report', report.name).send() diff --git a/frappe/email/doctype/auto_email_report/test_auto_email_report.py b/frappe/email/doctype/auto_email_report/test_auto_email_report.py new file mode 100644 index 0000000000..72da43098b --- /dev/null +++ b/frappe/email/doctype/auto_email_report/test_auto_email_report.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest, json + +# test_records = frappe.get_test_records('Auto Email Report') + +class TestAutoEmailReport(unittest.TestCase): + pass \ No newline at end of file diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index a4d414a1fb..7e0398d20d 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -146,25 +146,37 @@ class EmailAccount(Document): else: raise + # reset failed attempts count + self.set_failed_attempts_count(0) + return email_server def handle_incoming_connect_error(self, description): - self.db_set("enable_incoming", 0) + '''Disable email account if 3 failed attempts found''' + if self.get_failed_attempts_count() == 3: + self.db_set("enable_incoming", 0) - for user in get_system_managers(only_name=True): - try: - assign_to.add({ - 'assign_to': user, - 'doctype': self.doctype, - 'name': self.name, - 'description': description, - 'priority': 'High', - 'notify': 1 - }) - except assign_to.DuplicateToDoError: - frappe.message_log.pop() - pass + for user in get_system_managers(only_name=True): + try: + assign_to.add({ + 'assign_to': user, + 'doctype': self.doctype, + 'name': self.name, + 'description': description, + 'priority': 'High', + 'notify': 1 + }) + except assign_to.DuplicateToDoError: + frappe.message_log.pop() + pass + else: + self.set_failed_attempts_count(self.get_failed_attempts_count() + 1) + + def set_failed_attempts_count(self, value): + frappe.cache().set('{0}:email-account-failed-attempts'.format(self.name), value) + def get_failed_attempts_count(self): + return cint(frappe.cache().get('{0}:email-account-failed-attempts'.format(self.name))) def receive(self, test_mails=None): """Called by scheduler to receive emails from this EMail account using POP3/IMAP.""" diff --git a/frappe/email/doctype/email_alert/email_alert.js b/frappe/email/doctype/email_alert/email_alert.js index 67b7737ed1..09783121b6 100755 --- a/frappe/email/doctype/email_alert/email_alert.js +++ b/frappe/email/doctype/email_alert/email_alert.js @@ -25,7 +25,8 @@ frappe.email_alert = { get_select_options(d) : null; })); var email_fields = $.map(fields, - function(d) { return d.options == "Email" ? + function(d) { return (d.options == "Email" || + (d.options=='User' && d.fieldtype=='Link')) ? get_select_options(d) : null; }); // set email recipient options @@ -48,6 +49,7 @@ frappe.ui.form.on("Email Alert", { }, refresh: function(frm) { frappe.email_alert.setup_fieldname_select(frm); + frm.get_field("is_standard").toggle(frappe.boot.developer_mode); frm.trigger('event'); }, document_type: function(frm) { diff --git a/frappe/email/doctype/email_alert/email_alert.json b/frappe/email/doctype/email_alert/email_alert.json index 709e252afb..63dc0b2815 100755 --- a/frappe/email/doctype/email_alert/email_alert.json +++ b/frappe/email/doctype/email_alert/email_alert.json @@ -1,18 +1,22 @@ { "allow_copy": 0, "allow_import": 0, - "allow_rename": 0, - "autoname": "field:subject", + "allow_rename": 1, + "autoname": "Prompt", + "beta": 0, "creation": "2014-07-11 17:18:09.923399", "custom": 0, "docstatus": 0, "doctype": "DocType", "document_type": "System", + "editable_grid": 0, + "engine": "InnoDB", "fields": [ { "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "1", "fieldname": "enabled", "fieldtype": "Check", @@ -38,6 +42,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "filters", "fieldtype": "Section Break", "hidden": 0, @@ -62,6 +67,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "To add dynamic subject, use jinja tags like\n\n
    {{ doc.name }} Delivered
    ", "fieldname": "subject", "fieldtype": "Data", @@ -87,6 +93,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "document_type", "fieldtype": "Link", "hidden": 0, @@ -112,6 +119,86 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, + "fieldname": "is_standard", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Is Standard", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "is_standard", + "fieldname": "module", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Module", + "length": 0, + "no_copy": 0, + "options": "Module Def", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "col_break_1", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, "fieldname": "event", "fieldtype": "Select", "hidden": 0, @@ -122,7 +209,7 @@ "label": "Send Alert On", "length": 0, "no_copy": 0, - "options": "\nNew\nSave\nSubmit\nCancel\nDays After\nDays Before\nValue Change", + "options": "\nNew\nSave\nSubmit\nCancel\nDays After\nDays Before\nValue Change\nMethod\nCustom", "permlevel": 0, "print_hide": 0, "print_hide_if_no_value": 0, @@ -137,6 +224,35 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.event=='Method'", + "description": "Trigger on valid methods like \"before_insert\", \"after_update\", etc (will depend on the DocType selected)", + "fieldname": "method", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Trigger Method", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, "depends_on": "eval:doc.event==\"Days After\" || doc.event==\"Days Before\"", "description": "Send alert if date matches this field's value", "fieldname": "date_changed", @@ -163,6 +279,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "0", "depends_on": "eval:doc.event==\"Days After\" || doc.event==\"Days Before\"", "description": "Send days before or after the reference date", @@ -190,6 +307,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:doc.event==\"Value Change\"", "description": "Send alert if this field's value changes", "fieldname": "value_changed", @@ -216,6 +334,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "section_break_9", "fieldtype": "Section Break", "hidden": 0, @@ -240,6 +359,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "", "description": "Optional: The alert will be sent if this expression is true", "fieldname": "condition", @@ -266,6 +386,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "column_break_6", "fieldtype": "Column Break", "hidden": 0, @@ -289,6 +410,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "html_7", "fieldtype": "HTML", "hidden": 0, @@ -313,6 +435,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "column_break_5", "fieldtype": "Section Break", "hidden": 0, @@ -337,6 +460,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "recipients", "fieldtype": "Table", "hidden": 0, @@ -362,6 +486,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "message_sb", "fieldtype": "Section Break", "hidden": 0, @@ -386,6 +511,9 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, + "default": "Add your message here", + "depends_on": "is_standard", "fieldname": "message", "fieldtype": "Text", "hidden": 0, @@ -401,7 +529,7 @@ "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, - "reqd": 1, + "reqd": 0, "search_index": 0, "set_only_once": 0, "unique": 0 @@ -410,6 +538,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "attach_print", "fieldtype": "Check", "hidden": 0, @@ -435,6 +564,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "message_examples", "fieldtype": "HTML", "hidden": 0, @@ -460,6 +590,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "view_properties", "fieldtype": "Button", "hidden": 0, @@ -486,6 +617,7 @@ "hide_toolbar": 0, "icon": "icon-envelope", "idx": 0, + "image_view": 0, "in_create": 0, "in_dialog": 0, "is_submittable": 0, @@ -493,7 +625,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2016-05-02 06:16:05.453253", + "modified": "2016-10-07 01:07:17.504744", "modified_by": "Administrator", "module": "Email", "name": "Email Alert", @@ -510,6 +642,7 @@ "export": 1, "if_owner": 0, "import": 0, + "is_custom": 0, "permlevel": 0, "print": 0, "read": 1, @@ -521,7 +654,6 @@ "write": 1 } ], - "quick_entry": 0, "read_only": 0, "read_only_onload": 0, diff --git a/frappe/email/doctype/email_alert/email_alert.py b/frappe/email/doctype/email_alert/email_alert.py index 7aaa5cea22..03aaa06da5 100755 --- a/frappe/email/doctype/email_alert/email_alert.py +++ b/frappe/email/doctype/email_alert/email_alert.py @@ -3,13 +3,19 @@ from __future__ import unicode_literals import frappe -import json +import json, os from frappe import _ from frappe.model.document import Document from frappe.utils import validate_email_add, nowdate from frappe.utils.jinja import validate_template +from frappe.modules.utils import export_module_json, get_doc_module +from markdown2 import markdown class EmailAlert(Document): + def autoname(self): + if not self.name: + self.name = self.subject + def validate(self): validate_template(self.subject) validate_template(self.message) @@ -23,6 +29,27 @@ class EmailAlert(Document): self.validate_forbidden_types() self.validate_condition() + def on_update(self): + frappe.cache().hdel('email_alerts', self.document_type) + path = export_module_json(self, self.is_standard, self.module) + if path: + # js + if not os.path.exists(path + '.md') and not os.path.exists(path + '.html'): + with open(path + '.md', 'w') as f: + f.write(self.message) + + # py + if not os.path.exists(path + '.py'): + with open(path + '.py', 'w') as f: + f.write("""from __future__ import unicode_literals + +import frappe + +def get_context(context): + # do your magic here + pass +""") + def validate_condition(self): temp_doc = frappe.new_doc(self.document_type) if self.condition: @@ -60,6 +87,67 @@ class EmailAlert(Document): return docs + def send(self, doc): + '''Build recipients and send email alert''' + context = get_context(doc) + + for recipient in self.recipients: + recipients = [] + if recipient.condition: + if not eval(recipient.condition, context): + continue + if recipient.email_by_document_field: + if validate_email_add(doc.get(recipient.email_by_document_field)): + recipients.append(doc.get(recipient.email_by_document_field)) + # else: + # print "invalid email" + if recipient.cc: + recipient.cc = recipient.cc.replace(",", "\n") + recipients = recipients + recipient.cc.split("\n") + + if not recipients: + return + + subject = self.subject + + context = {"doc": doc, "alert": self, "comments": None} + + if self.is_standard: + self.load_standard_properties(context) + + if doc.get("_comments"): + context["comments"] = json.loads(doc.get("_comments")) + + if "{" in subject: + subject = frappe.render_template(self.subject, context) + + frappe.sendmail(recipients=recipients, subject=subject, + message= frappe.render_template(self.message, context), + reference_doctype = doc.doctype, + reference_name = doc.name, + attachments = [frappe.attach_print(doc.doctype, doc.name)] if self.attach_print else None) + + def load_standard_properties(self, context): + module = get_doc_module(self.module, self.doctype, self.name) + if module: + if hasattr(module, 'get_context'): + out = module.get_context(context) + if out: context.update(out) + + def load_template(extn): + template_path = os.path.join(os.path.dirname(module.__file__), + frappe.scrub(self.name) + extn) + if os.path.exists(template_path): + with open(template_path, 'r') as f: + self.message = f.read() + return True + + # get template + if not load_template('.html'): + if load_template('.md'): + self.message = markdown(self.message) + + @frappe.whitelist() def get_documents_for_today(email_alert): email_alert = frappe.get_doc('Email Alert', email_alert) @@ -70,89 +158,42 @@ def trigger_daily_alerts(): trigger_email_alerts(None, "daily") def trigger_email_alerts(doc, method=None): - from jinja2 import TemplateError if frappe.flags.in_import or frappe.flags.in_patch: # don't send email alerts while syncing or patching return if method == "daily": - for alert in frappe.db.sql_list("""select name from `tabEmail Alert` where event in ('Days Before', 'Days After') and enabled=1"""): alert = frappe.get_doc("Email Alert", alert) for doc in alert.get_documents_for_today(): evaluate_alert(doc, alert, alert.event) - else: - if method in ("on_update", "validate") and doc.flags.in_insert: - # don't call email alerts multiple times for inserts - # on insert only "New" type alert must be called - return - - eevent = { - "on_update": "Save", - "after_insert": "New", - "validate": "Value Change", - "on_submit": "Submit", - "on_cancel": "Cancel", - }[method] - - for alert in frappe.db.sql_list("""select name from `tabEmail Alert` - where document_type=%s and event=%s and enabled=1""", (doc.doctype, eevent)): - try: - evaluate_alert(doc, alert, eevent) - except TemplateError: - frappe.throw(_("Error while evaluating Email Alert {0}. Please fix your template.").format(alert)) def evaluate_alert(doc, alert, event): - if isinstance(alert, basestring): - alert = frappe.get_doc("Email Alert", alert) - - context = get_context(doc) - - if alert.condition: - if not eval(alert.condition, context): - return - - if event=="Value Change" and not doc.is_new(): - if doc.get(alert.value_changed) == frappe.db.get_value(doc.doctype, - doc.name, alert.value_changed): - return # value not changed - - for recipient in alert.recipients: - recipients = [] - if recipient.condition: - if not eval(recipient.condition, context): - continue - if recipient.email_by_document_field: - if validate_email_add(doc.get(recipient.email_by_document_field)): - recipients.append(doc.get(recipient.email_by_document_field)) - # else: - # print "invalid email" - if recipient.cc: - recipient.cc = recipient.cc.replace(",", "\n") - recipients = recipients + recipient.cc.split("\n") - - if not recipients: - return - - subject = alert.subject + from jinja2 import TemplateError + try: + if isinstance(alert, basestring): + alert = frappe.get_doc("Email Alert", alert) - if event != "Value Change" and not doc.is_new(): - # reload the doc for the latest values & comments, - # except for validate type event. - doc = frappe.get_doc(doc.doctype, doc.name) + context = get_context(doc) - context = {"doc": doc, "alert": alert, "comments": None} + if alert.condition: + if not eval(alert.condition, context): + return - if doc.get("_comments"): - context["comments"] = json.loads(doc.get("_comments")) + if event=="Value Change" and not doc.is_new(): + if doc.get(alert.value_changed) == frappe.db.get_value(doc.doctype, + doc.name, alert.value_changed): + return # value not changed - if "{" in subject: - subject = frappe.render_template(alert.subject, context) + if event != "Value Change" and not doc.is_new(): + # reload the doc for the latest values & comments, + # except for validate type event. + doc = frappe.get_doc(doc.doctype, doc.name) - frappe.sendmail(recipients=recipients, subject=subject, - message= frappe.render_template(alert.message, context), reference_doctype = doc.doctype, reference_name = doc.name, - attachments = [frappe.attach_print(doc.doctype, doc.name)] if alert.attach_print else None) + alert.send(doc) + except TemplateError: + frappe.throw(_("Error while evaluating Email Alert {0}. Please fix your template.").format(alert)) def get_context(doc): return {"doc": doc, "nowdate": nowdate} diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json index 7b96fee6d7..0790dad9f2 100644 --- a/frappe/email/doctype/newsletter/newsletter.json +++ b/frappe/email/doctype/newsletter/newsletter.json @@ -2,7 +2,7 @@ "allow_copy": 0, "allow_import": 0, "allow_rename": 1, - "autoname": "field:subject", + "autoname": "", "beta": 0, "creation": "2013-01-10 16:34:31", "custom": 0, @@ -16,6 +16,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "email_group", "fieldtype": "Link", "hidden": 0, @@ -42,6 +43,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "subject", "fieldtype": "Small Text", "hidden": 0, @@ -66,6 +68,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "", "fieldname": "send_from", "fieldtype": "Data", @@ -91,6 +94,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "email_sent", "fieldtype": "Check", "hidden": 0, @@ -115,6 +119,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "newsletter_content", "fieldtype": "Section Break", "hidden": 0, @@ -139,6 +144,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "message", "fieldtype": "Text Editor", "hidden": 0, @@ -163,6 +169,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "", "fieldname": "test_the_newsletter", "fieldtype": "Section Break", @@ -188,6 +195,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "A Lead with this email id should exist", "fieldname": "test_email_id", "fieldtype": "Data", @@ -213,6 +221,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "test_send", "fieldtype": "Button", "hidden": 0, @@ -247,7 +256,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2016-08-05 06:26:58.705579", + "modified": "2016-09-13 11:33:47.892910", "modified_by": "Administrator", "module": "Email", "name": "Newsletter", diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index ab0eb636b6..1fa4b3ead7 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -15,6 +15,9 @@ from frappe.email.queue import send from frappe.email.doctype.email_group.email_group import add_subscribers class Newsletter(Document): + def autoname(self): + self.name = self.subject + def onload(self): if self.email_sent: self.get("__onload").status_count = dict(frappe.db.sql("""select status, count(name) diff --git a/frappe/email/doctype/standard_reply/standard_reply.py b/frappe/email/doctype/standard_reply/standard_reply.py index 308f45bb9b..dd29f275a2 100644 --- a/frappe/email/doctype/standard_reply/standard_reply.py +++ b/frappe/email/doctype/standard_reply/standard_reply.py @@ -4,9 +4,11 @@ from __future__ import unicode_literals import frappe, json from frappe.model.document import Document +from frappe.utils.jinja import validate_template class StandardReply(Document): - pass + def validate(self): + validate_template(self.response) @frappe.whitelist() def get_standard_reply(template_name, doc): diff --git a/frappe/email/queue.py b/frappe/email/queue.py index e07abd288a..93d0f00ffc 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -340,7 +340,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False): raise e else: - # log to scheduler log + # log to Error Log log('frappe.email.queue.flush', unicode(e)) def clear_outbox(): diff --git a/frappe/email/receive.py b/frappe/email/receive.py index fb57674587..39b970497f 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -76,7 +76,7 @@ class EmailServer: return True except _socket.error: - # log performs rollback and logs error in scheduler log + # log performs rollback and logs error in Error Log log("receive.connect_pop") # Invalid mail server -- due to refusing connection @@ -185,7 +185,7 @@ class EmailServer: raise LoginLimitExceeded, e else: - # log performs rollback and logs error in scheduler log + # log performs rollback and logs error in Error Log log("receive.get_messages", self.make_error_msg(msg_num, incoming_mail)) self.errors = True frappe.db.rollback() @@ -283,7 +283,7 @@ class Email: self.subject = self.subject.decode(_subject[0][1]) else: # assume that the encoding is utf-8 - self.subject = self.subject.decode("utf-8") + self.subject = self.subject.decode("utf-8")[:140] if not self.subject: self.subject = "No Subject" @@ -360,7 +360,7 @@ class Email: return part.get_payload() def get_attachment(self, part): - charset = self.get_charset(part) + #charset = self.get_charset(part) fcontent = part.get_payload(decode=True) if fcontent: diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index ca9c77e1c1..31b0acc856 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -66,6 +66,8 @@ def get_outgoing_email_account(raise_exception_not_set=True, append_to=None): frappe.OutgoingEmailError) if email_account: + if email_account.enable_outgoing and not getattr(email_account, 'from_site_config', False): + email_account.password = email_account.get_password() email_account.default_sender = email.utils.formataddr((email_account.name, email_account.get("email_id"))) frappe.local.outgoing_email_account[append_to or "default"] = email_account diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index 7cbee70727..6a6dced8d9 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -1,6 +1,7 @@ { "Afghanistan": { "code": "af", + "currency": "AFN", "currency_fraction": "Pul", "currency_fraction_units": 100, "currency_symbol": "\u060b", @@ -2453,8 +2454,10 @@ }, "Ukraine": { "code": "ua", + "currency": "UAH", "currency_fraction": "Kopiyka", "currency_fraction_units": 100, + "currency_name": "Ukrainian Hryvnia", "currency_symbol": "\u20b4", "number_format": "#,###.##", "timezones": [ @@ -2620,7 +2623,7 @@ }, "Zambia": { "code": "zm", - "currency": "ZMK", + "currency": "ZMW", "currency_fraction": "Ngwee", "currency_fraction_units": 100, "currency_name": "Zambian Kwacha", diff --git a/frappe/geo/languages.json b/frappe/geo/languages.json new file mode 100644 index 0000000000..149025d92a --- /dev/null +++ b/frappe/geo/languages.json @@ -0,0 +1,290 @@ +[ + { + "code": "am", + "name": "\u12a0\u121b\u122d\u129b" + }, + { + "code": "ar", + "name": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629" + }, + { + "code": "bg", + "name": "b\u01celgarski" + }, + { + "code": "bn", + "name": "\u09ac\u09be\u0999\u09be\u09b2\u09bf" + }, + { + "code": "bo", + "name": "\u0f63\u0fb7\u0f0b\u0f66\u0f60\u0f72\u0f0b\u0f66\u0f90\u0f51\u0f0b" + }, + { + "code": "bs", + "name": "bosanski" + }, + { + "code": "ca", + "name": "catal\u00e0" + }, + { + "code": "cs", + "name": "\u010desky" + }, + { + "code": "da", + "name": "dansk" + }, + { + "code": "da-DK", + "name": "Dansk (Danmark)" + }, + { + "code": "de", + "name": "deutsch" + }, + { + "code": "el", + "name": "\u03b5\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac" + }, + { + "code": "en", + "name": "english" + }, + { + "code": "en-US", + "name": "English (United States)" + }, + { + "code": "es", + "name": "espa\u00f1ol" + }, + { + "code": "es-AR", + "name": "Espa\u00f1ol (Argentina)" + }, + { + "code": "es-CL", + "name": "Espa\u00f1ol (Chile)" + }, + { + "code": "es-GT", + "name": "Espa\u00f1ol (Guatemala)" + }, + { + "code": "es-MX", + "name": "Espa\u00f1ol (M\u00e9xico)" + }, + { + "code": "es-NI", + "name": "Espa\u00f1ol (Nicaragua)" + }, + { + "code": "es-PE", + "name": "Espa\u00f1ol (Per\u00fa)" + }, + { + "code": "et", + "name": "eesti" + }, + { + "code": "fa", + "name": "\u067e\u0627\u0631\u0633\u06cc" + }, + { + "code": "fi", + "name": "suomalainen" + }, + { + "code": "fr", + "name": "fran\u00e7ais" + }, + { + "code": "fr-CA", + "name": "fran\u00e7ais canadien" + }, + { + "code": "gu", + "name": "\u0a97\u0ac1\u0a9c\u0ab0\u0abe\u0aa4\u0ac0" + }, + { + "code": "he", + "name": "\u05e2\u05d1\u05e8\u05d9\u05ea" + }, + { + "code": "hi", + "name": "\u0939\u093f\u0902\u0926\u0940" + }, + { + "code": "hr", + "name": "hrvatski" + }, + { + "code": "hu", + "name": "magyar" + }, + { + "code": "id", + "name": "Indonesia" + }, + { + "code": "is", + "name": "\u00edslenska" + }, + { + "code": "it", + "name": "italiano" + }, + { + "code": "ja", + "name": "\u65e5\u672c\u8a9e" + }, + { + "code": "km", + "name": "\u1797\u17b6\u179f\u17b6\u1781\u17d2\u1798\u17c2\u179a" + }, + { + "code": "kn", + "name": "\u0c95\u0ca8\u0ccd\u0ca8\u0ca1" + }, + { + "code": "ko", + "name": "\ud55c\uad6d\uc758" + }, + { + "code": "ku", + "name": "\u06a9\u0648\u0631\u062f\u06cc" + }, + { + "code": "lo", + "name": "\u0ea5\u0eb2\u0ea7" + }, + { + "code": "lt", + "name": "lietuvi\u0173 kalba" + }, + { + "code": "lv", + "name": "latvie\u0161u valoda" + }, + { + "code": "mk", + "name": "\u043c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438" + }, + { + "code": "ml", + "name": "\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02" + }, + { + "code": "mr", + "name": "\u092e\u0930\u093e\u0920\u0940" + }, + { + "code": "ms", + "name": "Melayu" + }, + { + "code": "my", + "name": "\u1019\u103c\u1014\u103a\u1019\u102c" + }, + { + "code": "nl", + "name": "nederlands" + }, + { + "code": "no", + "name": "norsk" + }, + { + "code": "pl", + "name": "polski" + }, + { + "code": "ps", + "name": "\u067e\u069a\u062a\u0648" + }, + { + "code": "pt", + "name": "portugu\u00eas" + }, + { + "code": "pt-BR", + "name": "portugu\u00eas brasileiro" + }, + { + "code": "ro", + "name": "rom\u00e2n" + }, + { + "code": "ru", + "name": "\u0440\u0443\u0441\u0441\u043a\u0438\u0439" + }, + { + "code": "rw", + "name": "Kinyarwanda" + }, + { + "code": "si", + "name": "\u0dc3\u0dd2\u0d82\u0dc4\u0dbd" + }, + { + "code": "sk", + "name": "sloven\u010dina (Slovak)" + }, + { + "code": "sl", + "name": "sloven\u0161\u010dina (Slovene)" + }, + { + "code": "sq", + "name": "shqiptar" + }, + { + "code": "sr", + "name": "\u0441\u0440\u043f\u0441\u043a\u0438" + }, + { + "code": "sr-SP", + "name": "srpski" + }, + { + "code": "sv", + "name": "svenska" + }, + { + "code": "ta", + "name": "\u0ba4\u0bae\u0bbf\u0bb4\u0bcd" + }, + { + "code": "te", + "name": "\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41" + }, + { + "code": "th", + "name": "\u0e44\u0e17\u0e22" + }, + { + "code": "tr", + "name": "T\u00fcrk" + }, + { + "code": "uk", + "name": "\u0443\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430" + }, + { + "code": "ur", + "name": "\u0627\u0631\u062f\u0648" + }, + { + "code": "vi", + "name": "vi\u1ec7t" + }, + { + "code": "zh", + "name": "\u7b80\u4f53\u4e2d\u6587" + }, + { + "code": "zh-TW", + "name": "\u7e41\u9ad4\u4e2d\u6587" + } +] \ No newline at end of file diff --git a/frappe/hooks.py b/frappe/hooks.py index 5361cea661..3211a2f96c 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -67,7 +67,7 @@ calendars = ["Event"] on_session_creation = [ "frappe.core.doctype.communication.feed.login_feed", - "frappe.core.doctype.user.user.notifify_admin_access_to_system_manager", + "frappe.core.doctype.user.user.notify_admin_access_to_system_manager", "frappe.limits.check_if_expired", "frappe.utils.scheduler.reset_enabled_scheduler_events", ] @@ -95,22 +95,13 @@ standard_queries = { doc_events = { "*": { - "after_insert": "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts", - "validate": [ - "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts", - ], "on_update": [ "frappe.desk.notifications.clear_doctype_notifications", - "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts", "frappe.core.doctype.communication.feed.update_feed" ], "after_rename": "frappe.desk.notifications.clear_doctype_notifications", - "on_submit": [ - "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts", - ], "on_cancel": [ "frappe.desk.notifications.clear_doctype_notifications", - "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts" ], "on_trash": "frappe.desk.notifications.clear_doctype_notifications" }, @@ -129,12 +120,13 @@ scheduler_events = { "hourly": [ "frappe.model.utils.link_count.update_link_count", 'frappe.model.utils.list_settings.sync_list_settings', - "frappe.utils.error.collect_error_snapshots" + "frappe.utils.error.collect_error_snapshots", + "frappe.integration_broker.doctype.integration_service.integration_service.trigger_integration_service_events" ], "daily": [ "frappe.email.queue.clear_outbox", "frappe.desk.notifications.clear_notifications", - "frappe.core.doctype.scheduler_log.scheduler_log.set_old_logs_as_seen", + "frappe.core.doctype.error_log.error_log.set_old_logs_as_seen", "frappe.desk.doctype.event.event.send_event_digest", "frappe.sessions.clear_expired_sessions", "frappe.email.doctype.email_alert.email_alert.trigger_daily_alerts", @@ -142,18 +134,21 @@ scheduler_events = { "frappe.utils.scheduler.disable_scheduler_on_expiry", "frappe.utils.scheduler.restrict_scheduler_events_if_dormant", "frappe.limits.update_space_usage", + "frappe.email.doctype.auto_email_report.auto_email_report.send_daily", + "frappe.desk.page.backups.backups.delete_downloadable_backups" ], "daily_long": [ - "frappe.integrations.doctype.dropbox_backup.dropbox_backup.take_backups_daily" + "frappe.integrations.dropbox_integration.take_backups_daily" ], "weekly_long": [ - "frappe.integrations.doctype.dropbox_backup.dropbox_backup.take_backups_weekly" + "frappe.integrations.dropbox_integration.take_backups_weekly" + ], + "monthly": [ + "frappe.email.doctype.auto_email_report.auto_email_report.send_monthly" ] } -default_background = "/assets/frappe/images/ui/into-the-dawn.jpg" - get_translated_dict = { ("doctype", "System Settings"): "frappe.geo.country_info.get_translated_dict", ("page", "setup-wizard"): "frappe.geo.country_info.get_translated_dict" @@ -180,3 +175,5 @@ bot_parsers = [ setup_wizard_exception = "frappe.desk.page.setup_wizard.setup_wizard.email_setup_wizard_exception" before_write_file = "frappe.limits.validate_space_limit" + +integration_services = ["PayPal", "Razorpay", "Dropbox", "LDAP"] diff --git a/frappe/installer.py b/frappe/installer.py index 23064ee2f2..c9be853dd3 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -17,6 +17,7 @@ from frappe.utils.fixtures import sync_fixtures from frappe.website import render from frappe.desk.doctype.desktop_icon.desktop_icon import sync_from_app from frappe.utils.password import create_auth_table +from frappe.modules.utils import sync_customizations def install_db(root_login="root", root_password=None, db_name=None, source_sql=None, admin_password=None, verbose=True, force=0, site_config=None, reinstall=False): @@ -131,18 +132,19 @@ def install_app(name, verbose=False, set_as_patched=True): sync_for(name, force=True, sync_everything=True, verbose=verbose) sync_from_app(name) - frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu() add_to_installed_apps(name) + frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu() + if set_as_patched: set_all_patches_as_completed(name) for after_install in app_hooks.after_install or []: frappe.get_attr(after_install)() - print "Installing fixtures..." sync_fixtures(name) + sync_customizations(name) frappe.flags.in_install = False @@ -265,21 +267,21 @@ def make_site_config(db_name=None, db_password=None, site_config=None): with open(site_file, "w") as f: f.write(json.dumps(site_config, indent=1, sort_keys=True)) -def update_site_config(key, value, validate=True): +def update_site_config(key, value, validate=True, site_config_path=None): """Update a value in site_config""" - with open(get_site_config_path(), "r") as f: + if not site_config_path: + site_config_path = get_site_config_path() + + with open(site_config_path, "r") as f: site_config = json.loads(f.read()) # In case of non-int value - if validate: - try: - value = int(value) - except ValueError: - pass + if value in ('0', '1'): + value = int(value) # boolean - if value in ("False", "True"): - value = eval(value) + if value in ("false", "true"): + value = eval(value.title()) # remove key if value is None if value == "None": @@ -288,7 +290,7 @@ def update_site_config(key, value, validate=True): else: site_config[key] = value - with open(get_site_config_path(), "w") as f: + with open(site_config_path, "w") as f: f.write(json.dumps(site_config, indent=1, sort_keys=True)) if frappe.local.conf: diff --git a/frappe/integrations/doctype/dropbox_backup/__init__.py b/frappe/integration_broker/__init__.py similarity index 100% rename from frappe/integrations/doctype/dropbox_backup/__init__.py rename to frappe/integration_broker/__init__.py diff --git a/frappe/docs/user/fr/guides/.txt b/frappe/integration_broker/doctype/__init__.py similarity index 100% rename from frappe/docs/user/fr/guides/.txt rename to frappe/integration_broker/doctype/__init__.py diff --git a/frappe/integration_broker/doctype/integration_request/__init__.py b/frappe/integration_broker/doctype/integration_request/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integration_broker/doctype/integration_request/integration_request.js b/frappe/integration_broker/doctype/integration_request/integration_request.js new file mode 100644 index 0000000000..4b3b9a2de7 --- /dev/null +++ b/frappe/integration_broker/doctype/integration_request/integration_request.js @@ -0,0 +1,8 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Integration Request', { + refresh: function(frm) { + + } +}); diff --git a/frappe/integration_broker/doctype/integration_request/integration_request.json b/frappe/integration_broker/doctype/integration_request/integration_request.json new file mode 100644 index 0000000000..e32450d9bb --- /dev/null +++ b/frappe/integration_broker/doctype/integration_request/integration_request.json @@ -0,0 +1,221 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-08-04 04:58:40.457416", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "integration_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Integration Type", + "length": 0, + "no_copy": 0, + "options": "\nHost\nRemote", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "integration_request_service", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Integration Request Service", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Queued", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Status", + "length": 0, + "no_copy": 0, + "options": "\nQueued\nAuthorized\nCompleted\nCancelled\nFailed\n", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "data", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Data", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "output", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Output", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "error", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Error", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2016-10-13 05:01:14.913553", + "modified_by": "Administrator", + "module": "Integration Broker", + "name": "Integration Request", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "integration_request_service", + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integration_broker/doctype/integration_request/integration_request.py b/frappe/integration_broker/doctype/integration_request/integration_request.py new file mode 100644 index 0000000000..2cb656459b --- /dev/null +++ b/frappe/integration_broker/doctype/integration_request/integration_request.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +import json + +class IntegrationRequest(Document): + def autoname(self): + if self.flags._name: + self.name = self.flags._name + + def update_status(self, params, status): + data = json.loads(self.data) + data.update(params) + + self.data = json.dumps(data) + self.status = status + self.save(ignore_permissions=True) + frappe.db.commit() diff --git a/frappe/integration_broker/doctype/integration_request/test_integration_request.py b/frappe/integration_broker/doctype/integration_request/test_integration_request.py new file mode 100644 index 0000000000..6b77b57de4 --- /dev/null +++ b/frappe/integration_broker/doctype/integration_request/test_integration_request.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +# test_records = frappe.get_test_records('Integration Request') + +class TestIntegrationRequest(unittest.TestCase): + pass diff --git a/frappe/integration_broker/doctype/integration_service/__init__.py b/frappe/integration_broker/doctype/integration_service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integration_broker/doctype/integration_service/integration_service.js b/frappe/integration_broker/doctype/integration_service/integration_service.js new file mode 100644 index 0000000000..04c407dc3e --- /dev/null +++ b/frappe/integration_broker/doctype/integration_service/integration_service.js @@ -0,0 +1,71 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.provide("frappe.integration_service"); + +{% include 'frappe/integrations/doctype/razorpay_settings/razorpay_settings.js' %} +{% include 'frappe/integrations/doctype/paypal_settings/paypal_settings.js' %} +{% include 'frappe/integrations/doctype/dropbox_settings/dropbox_settings.js' %} +{% include 'frappe/integrations/doctype/ldap_settings/ldap_settings.js' %} + +frappe.ui.form.on('Integration Service', { + onload: function(frm) { + frappe.call({ + method: "frappe.integration_broker.doctype.integration_service.integration_service.get_integration_services", + callback: function(r){ + set_field_options("service", r.message) + } + }) + }, + + refresh: function(frm){ + if (frm.doc.service){ + frm.events.setup_custom_buttons(frm); + frm.events.setup_service_details(frm); + } + }, + + setup_custom_buttons: function(frm) { + frm.add_custom_button(__("{0} Settings", [frm.doc.service]), function(){ + frappe.set_route("List", frm.doc.service + " Settings"); + }); + + frm.add_custom_button(__("Show Log"), function(){ + frappe.route_options = {"integration_request_service": frm.doc.service}; + frappe.set_route("List", "Integration Request"); + }); + }, + + setup_service_details: function(frm) { + var service_name = frm.doc.service.toLowerCase().replace(/ /g, "_"); + var service_handelr = service_name + "_settings"; + service_handelr = new frappe.integration_service[service_handelr](); + service_handelr.get_service_info(frm); + + var scheduler_job_info = service_handelr.get_scheduler_job_info(); + frm.events.render_scheduler_jobs(frm, scheduler_job_info); + + }, + + render_scheduler_jobs: function(frm, scheduler_job_info) { + var schduler_job_view = frm.fields_dict.scheduler_events_html.wrapper; + + $(schduler_job_view).empty(); + $(schduler_job_view).append('\ + \ + \ + \ + \ + \ + \ + \ + \ +
    Event Action
    ') + + $.each(scheduler_job_info, function(evnt, help){ + $row = $("").appendTo($(schduler_job_view).find("tbody")); + $("").appendTo($row).html(evnt).css("font-size", "12px"); + $("").appendTo($row).html(help).css("font-size", "12px"); + }) + } +}); diff --git a/frappe/integration_broker/doctype/integration_service/integration_service.json b/frappe/integration_broker/doctype/integration_service/integration_service.json new file mode 100644 index 0000000000..774a3d287f --- /dev/null +++ b/frappe/integration_broker/doctype/integration_service/integration_service.json @@ -0,0 +1,217 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:service", + "beta": 0, + "creation": "2016-08-01 12:59:46.408707", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "editable_grid": 1, + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "service", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Service", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Enabled", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "integration_service_help", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "background_events", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Background Events", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "scheduler_events_html", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2016-10-06 06:14:48.271314", + "modified_by": "Administrator", + "module": "Integration Broker", + "name": "Integration Service", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integration_broker/doctype/integration_service/integration_service.py b/frappe/integration_broker/doctype/integration_service/integration_service.py new file mode 100644 index 0000000000..c8f5c96f8b --- /dev/null +++ b/frappe/integration_broker/doctype/integration_service/integration_service.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.model.document import Document +import json, urlparse +from frappe.utils import get_request_session + +class IntegrationService(Document): + def on_update(self): + if self.enabled: + if not self.flags.ignore_mandatory: + self.enable_service() + self.install_fixtures() + + frappe.cache().delete_value('scheduler_events') + + def install_fixtures(self): + pass + + def enable_service(self): + service_doc = "{0} Settings".format(self.service) + frappe.get_doc(service_doc).enable() + + #rest request handler + def get_request(self, url, auth=None, data=None): + if not auth: + auth = '' + if not data: + data = {} + + try: + s = get_request_session() + frappe.flags.integration_request = s.get(url, data={}, auth=auth) + frappe.flags.integration_request.raise_for_status() + return frappe.flags.integration_request.json() + + except Exception, exc: + frappe.log_error(frappe.get_traceback()) + raise exc + + def post_request(self, url, auth=None, data=None): + if not auth: + auth = '' + if not data: + data = {} + try: + s = get_request_session() + res = s.post(url, data=data, auth=auth) + res.raise_for_status() + + if res.headers.get("content-type") == "text/plain; charset=utf-8": + return urlparse.parse_qs(res.text) + + return res.json() + except Exception, exc: + frappe.log_error() + raise exc + + def put_request(url, auth=None, data=None): + pass + + def create_request(self, data, integration_type, service_name, name=None): + if not isinstance(data, basestring): + data = json.dumps(data) + + integration_request = frappe.get_doc({ + "doctype": "Integration Request", + "integration_type": integration_type, + "integration_request_service": service_name, + "data": data + }) + + if name: + integration_request.flags._name = name + + integration_request.insert(ignore_permissions=True) + frappe.db.commit() + + return integration_request + +def get_integration_controller(service_name): + '''Returns integration controller module from app_name.integrations.{service}''' + try: + return frappe.get_doc("{0} Settings".format(service_name)) + except Exception: + frappe.throw(_("Module {service} not found".format(service=service_name))) + +@frappe.whitelist() +def get_integration_services(): + services = [""] + for app in frappe.get_installed_apps(): + services.extend(frappe.get_hooks("integration_services", app_name = app)) + + return services + +def get_integration_service_events(): + '''Get scheduler events for enabled integrations''' + events = {} + for service in frappe.get_all("Integration Service", filters={"enabled": 1}, + fields=["name"]): + controller = get_integration_controller(service.name) + + if hasattr(controller, "scheduler_events"): + for key, handlers in controller.scheduler_events.items(): + events.setdefault(key, []).extend(handlers) + + return events diff --git a/frappe/integration_broker/doctype/integration_service/test_integration_service.py b/frappe/integration_broker/doctype/integration_service/test_integration_service.py new file mode 100644 index 0000000000..e9f662e69f --- /dev/null +++ b/frappe/integration_broker/doctype/integration_service/test_integration_service.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from frappe.utils.scheduler import get_scheduler_events + +# test_records = frappe.get_test_records('Integration Service') + +class TestIntegrationService(unittest.TestCase): + def test_scheudler_events(self): + dropbox_settings = frappe.get_doc('Dropbox Settings') + dropbox_settings.db_set('enabled', 1) + + events = get_scheduler_events('daily_long') + self.assertTrue('frappe.integrations.dropbox_integration.take_backups_daily' in events) + + dropbox_settings.db_set('enabled', 0) diff --git a/frappe/integration_broker/doctype/integration_service_parameter/__init__.py b/frappe/integration_broker/doctype/integration_service_parameter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integration_broker/doctype/integration_service_parameter/integration_service_parameter.json b/frappe/integration_broker/doctype/integration_service_parameter/integration_service_parameter.json new file mode 100644 index 0000000000..6524fc411e --- /dev/null +++ b/frappe/integration_broker/doctype/integration_service_parameter/integration_service_parameter.json @@ -0,0 +1,113 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-08-01 12:51:31.838970", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "editable_grid": 1, + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "label", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Parameter", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "value", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Value", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "fieldname", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Fieldname", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2016-08-03 05:36:47.261367", + "modified_by": "Administrator", + "module": "Integration Broker", + "name": "Integration Service Parameter", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integration_broker/doctype/integration_service_parameter/integration_service_parameter.py b/frappe/integration_broker/doctype/integration_service_parameter/integration_service_parameter.py new file mode 100644 index 0000000000..e691b1d9e3 --- /dev/null +++ b/frappe/integration_broker/doctype/integration_service_parameter/integration_service_parameter.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class IntegrationServiceParameter(Document): + pass diff --git a/frappe/integration_broker/doctype/oauth_authorization_code/__init__.py b/frappe/integration_broker/doctype/oauth_authorization_code/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integration_broker/doctype/oauth_authorization_code/oauth_authorization_code.js b/frappe/integration_broker/doctype/oauth_authorization_code/oauth_authorization_code.js new file mode 100644 index 0000000000..32746e6752 --- /dev/null +++ b/frappe/integration_broker/doctype/oauth_authorization_code/oauth_authorization_code.js @@ -0,0 +1,8 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('OAuth Authorization Code', { + refresh: function(frm) { + + } +}); diff --git a/frappe/integration_broker/doctype/oauth_authorization_code/oauth_authorization_code.json b/frappe/integration_broker/doctype/oauth_authorization_code/oauth_authorization_code.json new file mode 100644 index 0000000000..35ee594063 --- /dev/null +++ b/frappe/integration_broker/doctype/oauth_authorization_code/oauth_authorization_code.json @@ -0,0 +1,241 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:authorization_code", + "beta": 0, + "creation": "2016-08-24 14:12:13.647159", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "client", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Client", + "length": 0, + "no_copy": 0, + "options": "OAuth Client", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "user", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "scopes", + "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Scopes", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "authorization_code", + "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Authorization Code", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "expiration_time", + "fieldtype": "Datetime", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Expiration time", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "redirect_uri_bound_to_authorization_code", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Redirect URI Bound To Auth Code", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "validity", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Validity", + "length": 0, + "no_copy": 0, + "options": "Valid\nInvalid", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2016-10-20 00:13:53.224437", + "modified_by": "Administrator", + "module": "Integration Broker", + "name": "OAuth Authorization Code", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integration_broker/doctype/oauth_authorization_code/oauth_authorization_code.py b/frappe/integration_broker/doctype/oauth_authorization_code/oauth_authorization_code.py new file mode 100644 index 0000000000..f08e7eb5bb --- /dev/null +++ b/frappe/integration_broker/doctype/oauth_authorization_code/oauth_authorization_code.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class OAuthAuthorizationCode(Document): + pass diff --git a/frappe/integration_broker/doctype/oauth_authorization_code/test_oauth_authorization_code.py b/frappe/integration_broker/doctype/oauth_authorization_code/test_oauth_authorization_code.py new file mode 100644 index 0000000000..cecf187e61 --- /dev/null +++ b/frappe/integration_broker/doctype/oauth_authorization_code/test_oauth_authorization_code.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +# test_records = frappe.get_test_records('OAuth Authorization Code') + +class TestOAuthAuthorizationCode(unittest.TestCase): + pass diff --git a/frappe/integration_broker/doctype/oauth_bearer_token/__init__.py b/frappe/integration_broker/doctype/oauth_bearer_token/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integration_broker/doctype/oauth_bearer_token/oauth_bearer_token.js b/frappe/integration_broker/doctype/oauth_bearer_token/oauth_bearer_token.js new file mode 100644 index 0000000000..da69753903 --- /dev/null +++ b/frappe/integration_broker/doctype/oauth_bearer_token/oauth_bearer_token.js @@ -0,0 +1,8 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('OAuth Bearer Token', { + refresh: function(frm) { + + } +}); diff --git a/frappe/integration_broker/doctype/oauth_bearer_token/oauth_bearer_token.json b/frappe/integration_broker/doctype/oauth_bearer_token/oauth_bearer_token.json new file mode 100644 index 0000000000..029b8754fe --- /dev/null +++ b/frappe/integration_broker/doctype/oauth_bearer_token/oauth_bearer_token.json @@ -0,0 +1,266 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:access_token", + "beta": 0, + "creation": "2016-08-24 14:10:17.471264", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "client", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Client", + "length": 0, + "no_copy": 0, + "options": "OAuth Client", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "user", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "scopes", + "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Scopes", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "access_token", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Access Token", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "refresh_token", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Refresh Token", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "expiration_time", + "fieldtype": "Datetime", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Expiration time", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "expires_in", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Expires In", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "status", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Status", + "length": 0, + "no_copy": 0, + "options": "Active\nRevoked", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2016-10-20 00:13:41.747141", + "modified_by": "Administrator", + "module": "Integration Broker", + "name": "OAuth Bearer Token", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integration_broker/doctype/oauth_bearer_token/oauth_bearer_token.py b/frappe/integration_broker/doctype/oauth_bearer_token/oauth_bearer_token.py new file mode 100644 index 0000000000..1c7e921140 --- /dev/null +++ b/frappe/integration_broker/doctype/oauth_bearer_token/oauth_bearer_token.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class OAuthBearerToken(Document): + def validate(self): + if not self.expiration_time: + self.expiration_time = frappe.utils.datetime.datetime.strptime(self.creation, "%Y-%m-%d %H:%M:%S.%f") + frappe.utils.datetime.timedelta(seconds=self.expires_in) + diff --git a/frappe/integration_broker/doctype/oauth_bearer_token/test_oauth_bearer_token.py b/frappe/integration_broker/doctype/oauth_bearer_token/test_oauth_bearer_token.py new file mode 100644 index 0000000000..af7de360ab --- /dev/null +++ b/frappe/integration_broker/doctype/oauth_bearer_token/test_oauth_bearer_token.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +# test_records = frappe.get_test_records('OAuth Bearer Token') + +class TestOAuthBearerToken(unittest.TestCase): + pass diff --git a/frappe/integration_broker/doctype/oauth_client/__init__.py b/frappe/integration_broker/doctype/oauth_client/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integration_broker/doctype/oauth_client/oauth_client.js b/frappe/integration_broker/doctype/oauth_client/oauth_client.js new file mode 100644 index 0000000000..b0caa562b1 --- /dev/null +++ b/frappe/integration_broker/doctype/oauth_client/oauth_client.js @@ -0,0 +1,8 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('OAuth Client', { + refresh: function(frm) { + + } +}); diff --git a/frappe/integration_broker/doctype/oauth_client/oauth_client.json b/frappe/integration_broker/doctype/oauth_client/oauth_client.json new file mode 100644 index 0000000000..83ec6bebdd --- /dev/null +++ b/frappe/integration_broker/doctype/oauth_client/oauth_client.json @@ -0,0 +1,428 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "", + "beta": 0, + "creation": "2016-08-24 14:07:21.955052", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "app_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "App Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "user", + "fieldtype": "Link", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_1", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "", + "fieldname": "client_id", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "App Client ID", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "If checked, users will not see the Confirm Access dialog.", + "fieldname": "skip_authorization", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Skip Authorization", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "", + "fieldname": "sb_1", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "A list of resources which the Client App will have access to after the user allows it.
    e.g. project", + "fieldname": "scopes", + "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Scopes", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_3", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "URIs for receiving authorization code once the user allows access, as well as failure responses. Typically a REST endpoint exposed by the Client App.\n
    e.g. http://hostname//api/method/frappe.www.login.login_via_facebook", + "fieldname": "redirect_uris", + "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Redirect URIs", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "default_redirect_uri", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Default Redirect URI", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "collapsible_depends_on": "1", + "columns": 0, + "fieldname": "sb_advanced", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": " Advanced Settings", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "grant_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Grant Type", + "length": 0, + "no_copy": 0, + "options": "Authorization Code\nImplicit\nResource Owner Password Credentials\nClient Credentials", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_2", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Code", + "fieldname": "response_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Response Type", + "length": 0, + "no_copy": 0, + "options": "Code\nToken", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2016-10-20 00:32:11.993940", + "modified_by": "Administrator", + "module": "Integration Broker", + "name": "OAuth Client", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "app_name", + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integration_broker/doctype/oauth_client/oauth_client.py b/frappe/integration_broker/doctype/oauth_client/oauth_client.py new file mode 100644 index 0000000000..3493832064 --- /dev/null +++ b/frappe/integration_broker/doctype/oauth_client/oauth_client.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class OAuthClient(Document): + def validate(self): + self.client_id = self.name diff --git a/frappe/integration_broker/doctype/oauth_client/test_oauth_client.py b/frappe/integration_broker/doctype/oauth_client/test_oauth_client.py new file mode 100644 index 0000000000..ee119455e5 --- /dev/null +++ b/frappe/integration_broker/doctype/oauth_client/test_oauth_client.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +# test_records = frappe.get_test_records('OAuth Client') + +class TestOAuthClient(unittest.TestCase): + pass diff --git a/frappe/integration_broker/integration_controller.py b/frappe/integration_broker/integration_controller.py new file mode 100644 index 0000000000..eed61178c5 --- /dev/null +++ b/frappe/integration_broker/integration_controller.py @@ -0,0 +1,12 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +from frappe.integration_broker.doctype.integration_service.integration_service import IntegrationService + +class IntegrationController(IntegrationService): + def __init__(self, setup=True): + '''Load the current controller data if setup is true''' + if setup: + super(IntegrationController, self).__init__('Integration Service', self.service_name) diff --git a/frappe/integration_broker/oauth2.py b/frappe/integration_broker/oauth2.py new file mode 100644 index 0000000000..422b6c9d4e --- /dev/null +++ b/frappe/integration_broker/oauth2.py @@ -0,0 +1,123 @@ +from __future__ import unicode_literals +import frappe, json +from frappe.oauth import OAuthWebRequestValidator, WebApplicationServer +from oauthlib.oauth2 import FatalClientError, OAuth2Error +from urllib import quote, urlencode +from urlparse import urlparse +from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings + +#Variables required across requests +oauth_validator = OAuthWebRequestValidator() +oauth_server = WebApplicationServer(oauth_validator) +credentials = None + +def get_urlparams_from_kwargs(param_kwargs): + arguments = param_kwargs + if arguments.get("data"): + arguments.pop("data") + if arguments.get("cmd"): + arguments.pop("cmd") + + return urlencode(arguments) + +@frappe.whitelist() +def approve(*args, **kwargs): + r = frappe.request + uri = r.url + http_method = r.method + body = r.get_data() + headers = r.headers + + try: + scopes, credentials = oauth_server.validate_authorization_request(uri, http_method, body, headers) + + headers, body, status = oauth_server.create_authorization_response(uri=credentials['redirect_uri'], \ + body=body, headers=headers, scopes=scopes, credentials=credentials) + uri = headers.get('Location', None) + + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = uri + + except FatalClientError as e: + return e + except OAuth2Error as e: + return e + +@frappe.whitelist(allow_guest=True) +def authorize(*args, **kwargs): + #Fetch provider URL from settings + oauth_settings = get_oauth_settings() + params = get_urlparams_from_kwargs(kwargs) + request_url = urlparse(frappe.request.url) + success_url = request_url.scheme + "://" + request_url.netloc + "/api/method/frappe.integration_broker.oauth2.approve?" + params + failure_url = frappe.form_dict["redirect_uri"] + "?error=access_denied" + + if frappe.session['user']=='Guest': + #Force login, redirect to preauth again. + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = "/login?redirect-to=/api/method/frappe.integration_broker.oauth2.authorize?" + quote(params) + + elif frappe.session['user']!='Guest': + try: + r = frappe.request + uri = r.url + http_method = r.method + body = r.get_data() + headers = r.headers + + scopes, credentials = oauth_server.validate_authorization_request(uri, http_method, body, headers) + + skip_auth = frappe.db.get_value("OAuth Client", credentials['client_id'], "skip_authorization") + unrevoked_tokens = frappe.get_all("OAuth Bearer Token", filters={"status":"Active"}) + + if skip_auth or (oauth_settings["skip_authorization"] == "Auto" and len(unrevoked_tokens)): + + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = success_url + else: + #Show Allow/Deny screen. + response_html_params = frappe._dict({ + "client_id": frappe.db.get_value("OAuth Client", kwargs['client_id'], "app_name"), + "success_url": success_url, + "failure_url": failure_url, + "details": scopes + }) + resp_html = frappe.render_template("templates/includes/oauth_confirmation.html", response_html_params) + frappe.respond_as_web_page("Confirm Access", resp_html) + + except FatalClientError as e: + return e + except OAuth2Error as e: + return e + +@frappe.whitelist(allow_guest=True) +def get_token(*args, **kwargs): + r = frappe.request + + uri = r.url + http_method = r.method + body = r.form + headers = r.headers + + try: + headers, body, status = oauth_server.create_token_response(uri, http_method, body, headers, credentials) + frappe.local.response = frappe._dict(json.loads(body)) + except FatalClientError as e: + return e + + +@frappe.whitelist(allow_guest=True) +def revoke_token(*args, **kwargs): + r = frappe.request + uri = r.url + http_method = r.method + body = r.form + headers = r.headers + + headers, body, status = oauth_server.create_revocation_response(uri, headers=headers, body=body, http_method=http_method) + + frappe.local.response['http_status_code'] = status + if status == 200: + return "success" + else: + return "bad request" \ No newline at end of file diff --git a/frappe/integrations/doctype/dropbox_backup/dropbox_backup.js b/frappe/integrations/doctype/dropbox_backup/dropbox_backup.js deleted file mode 100644 index 2a24e54f8d..0000000000 --- a/frappe/integrations/doctype/dropbox_backup/dropbox_backup.js +++ /dev/null @@ -1,46 +0,0 @@ -$.extend(cur_frm.cscript, { - onload_post_render: function() { - cur_frm.fields_dict.allow_dropbox_access.$input.addClass("btn-primary"); - }, - - refresh: function() { - if(!cur_frm.doc.allow_dropbox_access) { - cur_frm.disable_save(); - } - }, - - validate_send_notifications_to: function() { - if(!cur_frm.doc.send_notifications_to) { - msgprint(__("Please specify") + ": " + - __(frappe.meta.get_label(cur_frm.doctype, - "send_notifications_to"))); - return false; - } - - return true; - }, - - allow_dropbox_access: function() { - if(cur_frm.cscript.validate_send_notifications_to()) { - return frappe.call({ - method: "frappe.integrations.doctype.dropbox_backup.dropbox_backup.get_dropbox_authorize_url", - callback: function(r) { - if(!r.exc) { - cur_frm.set_value("dropbox_access_secret", r.message.secret); - cur_frm.set_value("dropbox_access_key", r.message.key); - cur_frm.save(null, function() { - window.open(r.message.url); - }); - } - } - }); - } - }, - - send_backups_to_dropbox: function() { - // on disable, save the form - if (!cur_frm.doc.send_backups_to_dropbox) { - cur_frm.save(); - } - } -}); diff --git a/frappe/integrations/doctype/dropbox_settings/__init__.py b/frappe/integrations/doctype/dropbox_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js new file mode 100644 index 0000000000..c9427e8183 --- /dev/null +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js @@ -0,0 +1,64 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.provide("frappe.integration_service") + +frappe.ui.form.on('Dropbox Settings', { + refresh: function(frm) { + frm.clear_custom_buttons(); + frm.events.take_backup(frm); + }, + + allow_dropbox_access: function(frm) { + if (frm.doc.app_access_key && frm.doc.app_secret_key) { + frappe.call({ + method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_dropbox_authorize_url", + freeze: true, + callback: function(r) { + if(!r.exc) { + frm.save(); + window.open(r.message.url); + } + } + }) + } + else { + frappe.msgprint(__("Please enter values for App Access Key and App Secret Key")) + } + }, + + take_backup: function(frm) { + if (frm.doc.app_access_key && frm.doc.app_secret_key && frm.doc.dropbox_access_key && frm.doc.dropbox_access_secret) { + frm.add_custom_button(__("Take Backup Now"), function(frm){ + frappe.call({ + method: "frappe.integrations.dropbox_integration.take_backup", + freeze: true + }) + }).addClass("btn-primary") + } + } +}); + +frappe.integration_service.dropbox_settings = Class.extend({ + init: function(frm) { + + }, + + get_scheduler_job_info: function() { + return { + "Daily": "Take backup of database and files to dropbox on daily basis", + "Weekly": "Take backup of database and files to dropbox on weekly basis" + } + }, + + get_service_info: function(frm) { + frappe.call({ + method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_service_details", + callback: function(r){ + var integration_service_help = frm.fields_dict.integration_service_help.wrapper; + $(integration_service_help).empty(); + $(integration_service_help).append(r.message); + } + }) + } +}) diff --git a/frappe/integrations/doctype/dropbox_backup/dropbox_backup.json b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json similarity index 71% rename from frappe/integrations/doctype/dropbox_backup/dropbox_backup.json rename to frappe/integrations/doctype/dropbox_settings/dropbox_settings.json index 938201cdf1..e597410033 100644 --- a/frappe/integrations/doctype/dropbox_backup/dropbox_backup.json +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json @@ -2,30 +2,36 @@ "allow_copy": 0, "allow_import": 0, "allow_rename": 0, - "creation": "2015-09-24 01:16:21.711868", + "beta": 0, + "creation": "2016-09-21 10:12:57.399174", "custom": 0, "docstatus": 0, "doctype": "DocType", - "document_type": "Setup", + "document_type": "System", + "editable_grid": 1, "fields": [ { "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "send_backups_to_dropbox", - "fieldtype": "Check", + "columns": 0, + "fieldname": "send_notifications_to", + "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, + "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 0, - "label": "Send Backups to Dropbox", + "label": "Send Notifications To", + "length": 0, "no_copy": 0, "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, - "reqd": 0, + "reqd": 1, "search_index": 0, "set_only_once": 0, "unique": 0 @@ -34,23 +40,25 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "default": "Daily", - "depends_on": "send_backups_to_dropbox", - "fieldname": "upload_backups_to_dropbox", + "columns": 0, + "fieldname": "backup_frequency", "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, + "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 0, - "label": "Upload Frequency", + "label": "Backup Frequency", + "length": 0, "no_copy": 0, - "options": "Daily\nWeekly", + "options": "\nDaily\nWeekly", "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, - "reqd": 0, + "reqd": 1, "search_index": 0, "set_only_once": 0, "unique": 0 @@ -59,21 +67,24 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "depends_on": "send_backups_to_dropbox", - "fieldname": "send_notifications_to", + "columns": 0, + "fieldname": "app_access_key", "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, + "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 0, - "label": "Send Notifications To", + "label": "App Access Key", + "length": 0, "no_copy": 0, "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, - "reqd": 0, + "reqd": 1, "search_index": 0, "set_only_once": 0, "unique": 0 @@ -82,20 +93,24 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "dropbox_access_key", - "fieldtype": "Data", - "hidden": 1, + "columns": 0, + "fieldname": "app_secret_key", + "fieldtype": "Password", + "hidden": 0, "ignore_user_permissions": 0, + "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 0, - "label": "Dropbox Access Key", + "label": "App Secret Key", + "length": 0, "no_copy": 0, "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, - "reqd": 0, + "reqd": 1, "search_index": 0, "set_only_once": 0, "unique": 0 @@ -104,17 +119,21 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "dropbox_access_secret", - "fieldtype": "Data", - "hidden": 1, + "columns": 0, + "fieldname": "allow_dropbox_access", + "fieldtype": "Button", + "hidden": 0, "ignore_user_permissions": 0, + "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 0, - "label": "Dropbox Access Secret", + "label": "Allow Dropbox Access", + "length": 0, "no_copy": 0, "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -126,18 +145,22 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "dropbox_access_allowed", - "fieldtype": "Data", + "columns": 0, + "fieldname": "dropbox_access_key", + "fieldtype": "Password", "hidden": 1, "ignore_user_permissions": 0, + "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 0, - "label": "Dropbox Access Allowed", + "label": "Dropbox Access Key", + "length": 0, "no_copy": 0, "permlevel": 0, "precision": "", "print_hide": 0, - "read_only": 0, + "print_hide_if_no_value": 0, + "read_only": 1, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -148,19 +171,22 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "depends_on": "send_backups_to_dropbox", - "fieldname": "allow_dropbox_access", - "fieldtype": "Button", - "hidden": 0, + "columns": 0, + "fieldname": "dropbox_access_secret", + "fieldtype": "Password", + "hidden": 1, "ignore_user_permissions": 0, + "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 0, - "label": "Allow Dropbox Access", + "label": "Dropbox Access Secret", + "length": 0, "no_copy": 0, "permlevel": 0, "precision": "", "print_hide": 0, - "read_only": 0, + "print_hide_if_no_value": 0, + "read_only": 1, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -170,15 +196,18 @@ ], "hide_heading": 0, "hide_toolbar": 0, - "in_create": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, "in_dialog": 0, "is_submittable": 0, "issingle": 1, "istable": 0, - "modified": "2015-09-24 01:42:25.670481", + "max_attachments": 0, + "modified": "2016-09-22 03:55:40.716643", "modified_by": "Administrator", "module": "Integrations", - "name": "Dropbox Backup", + "name": "Dropbox Settings", "name_case": "", "owner": "Administrator", "permissions": [ @@ -189,7 +218,7 @@ "create": 1, "delete": 1, "email": 1, - "export": 0, + "export": 1, "if_owner": 0, "import": 0, "permlevel": 0, @@ -203,8 +232,10 @@ "write": 1 } ], - "read_only": 0, + "quick_entry": 0, + "read_only": 1, "read_only_onload": 0, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "track_seen": 0 } \ No newline at end of file diff --git a/frappe/integrations/doctype/dropbox_backup/dropbox_backup.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py similarity index 51% rename from frappe/integrations/doctype/dropbox_backup/dropbox_backup.py rename to frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index c9f7dd1539..c02bbf4a8f 100644 --- a/frappe/integrations/doctype/dropbox_backup/dropbox_backup.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -1,30 +1,158 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2015, Frappe Technologies and contributors # For license information, please see license.txt -# SETUP: -# install pip install --upgrade dropbox -# -# Create new Dropbox App -# -# in conf.py, set oauth2 settings -# dropbox_access_key -# dropbox_access_secret - from __future__ import unicode_literals import frappe -from frappe.model.document import Document -from frappe.utils import (cint, split_emails, get_request_site_address, cstr, - get_files_path, get_backups_path, encode) -from frappe.utils.backups import new_backup - import os from frappe import _ +from frappe.utils.backups import new_backup +from frappe.utils.background_jobs import enqueue +from frappe.utils import (cint, split_emails, get_request_site_address, cstr, + get_files_path, get_backups_path, encode) +from frappe.integration_broker.doctype.integration_service.integration_service import IntegrationService ignore_list = [".DS_Store"] -class DropboxBackup(Document): - pass +class DropboxSettings(IntegrationService): + scheduler_events = { + "daily_long": [ + "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily" + ], + "weekly_long": [ + "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_weekly" + ] + } + + def validate(self): + if not self.flags.ignore_mandatory: + self.validate_dropbox_credentails() + + def on_update(self): + pass + + def enable(self): + """ enable service """ + if not self.flags.ignore_mandatory: + self.validate_dropbox_credentails() + + def validate_dropbox_credentails(self): + try: + self.get_dropbox_session() + except Exception, e: + frappe.throw(e.message) + + def get_dropbox_session(self): + try: + from dropbox import session + except: + raise Exception(_("Please install dropbox python module")) + + if not (self.app_access_key or self.app_secret_key): + raise Exception(_("Please set Dropbox access keys in your site config")) + + sess = session.DropboxSession(self.app_access_key, + self.get_password(fieldname="app_secret_key", raise_exception=False), "app_folder") + + return sess + +@frappe.whitelist() +def get_service_details(): + return """ +
    + Steps to enable dropbox backup service: +
      +
    1. Create a dropbox app then get App Key and App Secret, + + https://www.dropbox.com/developers/apps + +
    2. +
      +
    3. Setup credentials on Dropbox Settings doctype. + Click on + + top right corner +
    4. +
      +
    5. After settings up App key and App Secret, generate access token + +
    6. +
      +
    7. + After saving settings, + + Dropbox Integration Service and Save a document. +
    8. +
    +

    + After enabling service, system will take backup of files and database on daily or weekly basis + as per set on Dropbox Settings page and upload it to your dropbox. +

    +
    + """ + +#get auth token +@frappe.whitelist() +def get_dropbox_authorize_url(): + doc = frappe.get_doc("Dropbox Settings") + sess = doc.get_dropbox_session() + request_token = sess.obtain_request_token() + + doc.update({ + "dropbox_access_key": request_token.key, + "dropbox_access_secret": request_token.secret + }) + + doc.save(ignore_permissions=False) + + return_address = get_request_site_address(True) \ + + "?cmd=frappe.integrations.doctype.dropbox_settings.dropbox_settings.dropbox_callback" + + url = sess.build_authorize_url(request_token, return_address) + + return { + "url": url, + "dropbox_access_key": request_token.key, + "dropbox_access_secret": request_token.secret + } + +@frappe.whitelist(allow_guest=True) +def dropbox_callback(oauth_token=None, not_approved=False): + doc = frappe.get_doc("Dropbox Settings") + + if not not_approved: + if doc.get_password(fieldname="dropbox_access_key", raise_exception=False)==oauth_token: + sess = doc.get_dropbox_session() + sess.set_request_token(doc.get_password(fieldname="dropbox_access_key", raise_exception=False), + doc.get_password(fieldname="dropbox_access_secret", raise_exception=False)) + + access_token = sess.obtain_access_token() + + frappe.db.set_value("Dropbox Settings", None, "dropbox_access_key", access_token.key) + frappe.db.set_value("Dropbox Settings", None, "dropbox_access_secret", access_token.secret) + + frappe.db.commit() + else: + frappe.respond_as_web_page(_("Dropbox Approval"), _("Illegal Access Token Please try again.

    Please close this window.Please close this window.Please close this window. max_packet_size: + uploader = dropbox_client.get_chunked_uploader(f, size) + while uploader.offset < size: + try: + uploader.upload_chunked() + uploader.finish(folder + "/" + os.path.basename(filename), overwrite=True) + + except rest.ErrorResponse, e: + # if "[401] u'Access token not found.'", + # it means that the user needs to again allow dropbox backup from the UI + # so re-raise + exc_message = cstr(e) + if (exc_message.startswith("[401]") + and dropbox_client.connection_reset_count < 10 + and exc_message != "[401] u'Access token not found.'"): + + + # session expired, so get a new connection! + # [401] u"The given OAuth 2 access token doesn't exist or has expired." + dropbox_client = get_dropbox_client(dropbox_client) + + else: + raise + else: + dropbox_client.put_file(folder + "/" + os.path.basename(filename), f, overwrite=True) return dropbox_client @@ -197,7 +303,7 @@ def upload_from_folder(path, dropbox_folder, dropbox_client, did_not_upload, err found = False filepath = os.path.join(path, filename) for file_metadata in response["contents"]: - if (os.path.basename(filepath) == os.path.basename(file_metadata["path"]) + if (os.path.basename(filepath) == os.path.basename(file_metadata["path"]) and os.stat(encode(filepath)).st_size == int(file_metadata["bytes"])): found = True break @@ -210,54 +316,3 @@ def upload_from_folder(path, dropbox_folder, dropbox_client, did_not_upload, err error_log.append(frappe.get_traceback()) return dropbox_client - -def get_dropbox_session(): - try: - from dropbox import session - except: - frappe.msgprint(_("Please install dropbox python module"), raise_exception=1) - - if not (frappe.conf.dropbox_access_key or frappe.conf.dropbox_secret_key): - frappe.throw(_("Please set Dropbox access keys in your site config")) - - sess = session.DropboxSession(frappe.conf.dropbox_access_key, frappe.conf.dropbox_secret_key, "app_folder") - return sess - -def upload_file_to_dropbox(filename, folder, dropbox_client): - from dropbox import rest - size = os.stat(encode(filename)).st_size - - with open(filename, 'r') as f: - # if max packet size reached, use chunked uploader - max_packet_size = 4194304 - - if size > max_packet_size: - uploader = dropbox_client.get_chunked_uploader(f, size) - while uploader.offset < size: - try: - uploader.upload_chunked() - uploader.finish(folder + "/" + os.path.basename(filename), overwrite=True) - - except rest.ErrorResponse, e: - # if "[401] u'Access token not found.'", - # it means that the user needs to again allow dropbox backup from the UI - # so re-raise - exc_message = cstr(e) - if (exc_message.startswith("[401]") - and dropbox_client.connection_reset_count < 10 - and exc_message != "[401] u'Access token not found.'"): - - - # session expired, so get a new connection! - # [401] u"The given OAuth 2 access token doesn't exist or has expired." - dropbox_client = get_dropbox_client(dropbox_client) - - else: - raise - else: - dropbox_client.put_file(folder + "/" + os.path.basename(filename), f, overwrite=True) - - return dropbox_client - -if __name__=="__main__": - backup_to_dropbox() diff --git a/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py new file mode 100644 index 0000000000..40540af8b8 --- /dev/null +++ b/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +# test_records = frappe.get_test_records('Dropbox Settings') + +class TestDropboxSettings(unittest.TestCase): + pass diff --git a/frappe/integrations/doctype/ldap_settings/__init__.py b/frappe/integrations/doctype/ldap_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.js b/frappe/integrations/doctype/ldap_settings/ldap_settings.js new file mode 100644 index 0000000000..81b8daa745 --- /dev/null +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.js @@ -0,0 +1,31 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.provide("frappe.integration_service") + +frappe.ui.form.on('LDAP Settings', { + refresh: function(frm) { + + } +}); + +frappe.integration_service.ldap_settings = Class.extend({ + init: function(frm) { + + }, + + get_scheduler_job_info: function() { + return {} + }, + + get_service_info: function(frm) { + frappe.call({ + method: "frappe.integrations.doctype.ldap_settings.ldap_settings.get_service_details", + callback: function(r){ + var integration_service_help = frm.fields_dict.integration_service_help.wrapper; + $(integration_service_help).empty(); + $(integration_service_help).append(r.message); + } + }) + } +}) \ No newline at end of file diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.json b/frappe/integrations/doctype/ldap_settings/ldap_settings.json new file mode 100644 index 0000000000..a9166b91e5 --- /dev/null +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.json @@ -0,0 +1,162 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-09-22 04:16:48.829658", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "editable_grid": 1, + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "ldap_server_url", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "LDAP Server Url", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "organizational_unit", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Organizational Unit", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "base_dn", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Base Distinguished Name (DN)", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "password", + "fieldtype": "Password", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Password for Base DN", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2016-09-22 04:31:42.998441", + "modified_by": "Administrator", + "module": "Integrations", + "name": "LDAP Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 1, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py new file mode 100644 index 0000000000..60b4a08640 --- /dev/null +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import cstr, cint +from frappe.integration_broker.doctype.integration_service.integration_service import IntegrationService + +class LDAPSettings(IntegrationService): + def on_update(self): + pass + + def validate(self): + if not self.flags.ignore_mandatory: + self.validate_ldap_credentails() + + def enable(self): + if not self.flags.ignore_mandatory: + self.validate_ldap_credentails() + + def validate_ldap_credentails(self): + try: + import ldap + conn = ldap.initialize(self.ldap_server_url) + conn.simple_bind_s(self.base_dn, self.get_password(raise_exception=False)) + except ImportError: + msg = """ +

    + Seems ldap is not installed on system.
    + Guidelines to install ldap dependancies and python package, + Click here, + +
    + """ + frappe.throw(msg, title="LDAP Not Installed") + + except ldap.LDAPError: + conn.unbind_s() + frappe.throw("Incorrect UserId or Password") + +@frappe.whitelist() +def get_service_details(): + return """ +
    +

    Steps to configure Service +

      +
    1. Setup credentials on LDAP settings doctype + Click on + + top right corner +
      + To setup LDAP you need, +
        +
      • Server URL & Port : ldap://ldap.forumsys.com:389
      • +
      • Base Distinguished Name : cn=read-only-admin,dc=example,dc=com
      • +
      • Organisational Unit : ou=mathematicians,dc=example,dc=com
      • +
      • Password : Base DN password
      • +
      +
    2. +
      +
    3. + After saving settings, + + LDAP Integration Service and Save a document. +
    4. +
    +
    + """ + +def get_ldap_settings(): + try: + settings = frappe.get_doc("LDAP Settings") + + settings.update({ + "enabled": cint(frappe.db.get_value("Integration Service", "LDAP", "enabled")), + "method": "frappe.integrations.doctype.ldap_settings.ldap_settings.login" + }) + return settings + except Exception: + # this will return blank settings + return frappe._dict() + +@frappe.whitelist(allow_guest=True) +def login(): + #### LDAP LOGIN LOGIC ##### + args = frappe.form_dict + user = authenticate_ldap_user(args.usr, args.pwd) + + frappe.local.login_manager.user = user.name + frappe.local.login_manager.post_login() + + # because of a GET request! + frappe.db.commit() + +def authenticate_ldap_user(user=None, password=None): + dn = None + params = {} + settings = get_ldap_settings() + + try: + import ldap + except: + msg = """ +
    + {{_("Seems ldap is not installed on system")}}.
    + Click here, + {{_("Guidelines to install ldap dependancies and python")}} +
    + """ + frappe.throw(msg, title="LDAP Not Installed") + + conn = ldap.initialize(settings.ldap_server_url) + + try: + # simple_bind_s is synchronous binding to server, it takes two param DN and password + conn.simple_bind_s(settings.base_dn, settings.get_password(raise_exception=False)) + + #search for surnames beginning with a + #available options for how deep a search you want. + #LDAP_SCOPE_BASE, LDAP_SCOPE_ONELEVEL,LDAP_SCOPE_SUBTREE, + result = conn.search_s(settings.organizational_unit, ldap.SCOPE_SUBTREE, + "uid=*{0}".format(user)) + + print result + + for dn, r in result: + dn = cstr(dn) + params["email"] = cstr(r['mail'][0]) + params["username"] = cstr(r['uid'][0]) + params["first_name"] = cstr(r['cn'][0]) + + if dn: + conn.simple_bind_s(dn, password) + return create_user(params) + else: + frappe.throw(_("Not a valid LDAP user")) + + except ldap.LDAPError: + conn.unbind_s() + frappe.throw(_("Incorrect UserId or Password")) + +def create_user(params): + if frappe.db.exists("User", params["email"]): + return frappe.get_doc("User", params["email"]) + + else: + params.update({ + "doctype": "User", + "send_welcome_email": 0, + "language": "", + "user_type": "System User", + "user_roles": [{ + "role": _("Blogger") + }] + }) + + user = frappe.get_doc(params).insert(ignore_permissions=True) + frappe.db.commit() + + return user \ No newline at end of file diff --git a/frappe/integrations/doctype/oauth_provider_settings/__init__.py b/frappe/integrations/doctype/oauth_provider_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.js b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.js new file mode 100644 index 0000000000..6d7d071934 --- /dev/null +++ b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('OAuth Provider Settings', { + refresh: function(frm) { + + } +}); diff --git a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.json b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.json new file mode 100644 index 0000000000..b48c50a000 --- /dev/null +++ b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.json @@ -0,0 +1,87 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-09-03 11:42:42.575525", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "skip_authorization", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Skip Authorization", + "length": 0, + "no_copy": 0, + "options": "Force\nAuto", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2016-10-20 00:13:21.988739", + "modified_by": "Administrator", + "module": "Integrations", + "name": "OAuth Provider Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py new file mode 100644 index 0000000000..2bf086e0fe --- /dev/null +++ b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe import _ + +class OAuthProviderSettings(Document): + pass + +def get_oauth_settings(): + """Returns oauth settings""" + out = frappe._dict({ + "skip_authorization" : frappe.db.get_value("OAuth Provider Settings", None, "skip_authorization") + }) + + return out \ No newline at end of file diff --git a/frappe/integrations/doctype/paypal_settings/__init__.py b/frappe/integrations/doctype/paypal_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/paypal_settings/paypal_settings.js b/frappe/integrations/doctype/paypal_settings/paypal_settings.js new file mode 100644 index 0000000000..602b04743f --- /dev/null +++ b/frappe/integrations/doctype/paypal_settings/paypal_settings.js @@ -0,0 +1,31 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.provide("frappe.integration_service") + +frappe.ui.form.on('PayPal Settings', { + refresh: function(frm) { + + } +}); + +frappe.integration_service.paypal_settings = Class.extend({ + init: function(frm) { + + }, + + get_scheduler_job_info: function() { + return {} + }, + + get_service_info: function(frm) { + frappe.call({ + method: "frappe.integrations.doctype.paypal_settings.paypal_settings.get_service_details", + callback: function(r){ + var integration_service_help = frm.fields_dict.integration_service_help.wrapper; + $(integration_service_help).empty(); + $(integration_service_help).append(r.message); + } + }) + } +}) \ No newline at end of file diff --git a/frappe/integrations/doctype/paypal_settings/paypal_settings.json b/frappe/integrations/doctype/paypal_settings/paypal_settings.json new file mode 100644 index 0000000000..44e032d0cf --- /dev/null +++ b/frappe/integrations/doctype/paypal_settings/paypal_settings.json @@ -0,0 +1,190 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-09-21 08:03:01.009852", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "editable_grid": 1, + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "api_username", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "API Username", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "api_password", + "fieldtype": "Password", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "API Password", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "signature", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Signature", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Check this if you are testing your payment using the Sandbox API", + "fieldname": "paypal_sandbox", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Use Sandbox", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Mention transaction completion page URL", + "fieldname": "redirect_to", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Redirect To", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2016-09-26 02:00:45.145155", + "modified_by": "Administrator", + "module": "Integrations", + "name": "PayPal Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 1, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/paypal_settings/paypal_settings.py b/frappe/integrations/doctype/paypal_settings/paypal_settings.py new file mode 100644 index 0000000000..56b59dcbb4 --- /dev/null +++ b/frappe/integrations/doctype/paypal_settings/paypal_settings.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from frappe import _ +from frappe.utils import get_url, call_hook_method +from urllib import urlencode +from frappe.integration_broker.doctype.integration_service.integration_service import IntegrationService +import urllib + +""" +# Integrating PayPal + +### 1. Validate Currency Support + +Example: + + from frappe.integration_broker.doctype.integration_service.integration_service import get_integration_controller + + controller = get_integration_controller("PayPal") + controller().validate_transaction_currency(currency) + +### 2. Redirect for payment + +Example: + + payment_details = { + "amount": 600, + "title": "Payment for bill : 111", + "description": "payment via cart", + "reference_doctype": "Payment Request", + "reference_docname": "PR0001", + "payer_email": "NuranVerkleij@example.com", + "payer_name": "Nuran Verkleij", + "order_id": "111", + "currency": "USD" + } + + # redirect the user to this url + url = controller().get_payment_url(**payment_details) + + +### 3. On Completion of Payment + +Write a method for `on_payment_authorized` in the reference doctype + +Example: + + def on_payment_authorized(payment_status): + # your code to handle callback + +##### Note: + +payment_status - payment gateway will put payment status on callback. +For paypal payment status parameter is one from: [Completed, Cancelled, Failed] + + +More Details: +
    For details on how to get your API credentials, follow this link: https://developer.paypal.com/docs/classic/api/apiCredentials/
    + +""" + +class PayPalSettings(IntegrationService): + service_name = "PayPal" + + supported_currencies = ["AUD", "BRL", "CAD", "CZK", "DKK", "EUR", "HKD", "HUF", "ILS", "JPY", "MYR", "MXN", + "TWD", "NZD", "NOK", "PHP", "PLN", "GBP", "RUB", "SGD", "SEK", "CHF", "THB", "TRY", "USD"] + + def validate(self): + if not self.flags.ignore_mandatory: + self.validate_paypal_credentails() + + def on_update(self): + pass + + def enable(self): + call_hook_method('payment_gateway_enabled', gateway=self.service_name) + if not self.flags.ignore_mandatory: + self.validate_paypal_credentails() + + def validate_transaction_currency(self, currency): + if currency not in self.supported_currencies: + frappe.throw(_("Please select another payment method. {0} does not support transactions in currency '{1}'").format(self.service_name, currency)) + + def get_paypal_params_and_url(self): + params = { + "USER": self.api_username, + "PWD": self.get_password(fieldname="api_password", raise_exception=False), + "SIGNATURE": self.signature, + "VERSION": "98", + "METHOD": "GetPalDetails" + } + + api_url = "https://api-3t.sandbox.paypal.com/nvp" if self.paypal_sandbox else "https://api-3t.paypal.com/nvp" + + return params, api_url + + def validate_paypal_credentails(self): + params, url = self.get_paypal_params_and_url() + params = urlencode(params) + + try: + res = self.post_request(url=url, data=params.encode("utf-8")) + + if res["ACK"][0] == "Failure": + raise Exception + + except Exception: + frappe.throw(_("Invalid payment gateway credentials")) + + def get_payment_url(self, **kwargs): + response = self.execute_set_express_checkout(kwargs["amount"], kwargs["currency"]) + + if self.paypal_sandbox: + return_url = "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token={0}" + else: + return_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token={0}" + + kwargs.update({ + "token": response.get("TOKEN")[0], + "correlation_id": response.get("CORRELATIONID")[0] + }) + + self.integration_request = self.create_request(kwargs, "Remote", self.service_name, response.get("TOKEN")[0]) + + return return_url.format(kwargs["token"]) + + def execute_set_express_checkout(self, amount, currency): + params, url = self.get_paypal_params_and_url() + params.update({ + "METHOD": "SetExpressCheckout", + "PAYMENTREQUEST_0_PAYMENTACTION": "SALE", + "PAYMENTREQUEST_0_AMT": amount, + "PAYMENTREQUEST_0_CURRENCYCODE": currency.upper(), + "returnUrl": get_url("/api/method/frappe.integrations.paypal.get_express_checkout_details"), + "cancelUrl": get_url("/payment-cancel") + }) + + params = urlencode(params) + + response = self.post_request(url, data=params.encode("utf-8")) + if response.get("ACK")[0] != "Success": + frappe.throw("Looks like something is wrong with this site's Paypal configuration.") + + return response + +@frappe.whitelist() +def get_service_details(): + return """ +
    +

    Steps to configure Service +

      +
    1. Get PayPal api credentials from link: + + https://developer.paypal.com/docs/classic/api/apiCredentials/ + +
    2. +
      +
    3. Setup credentials on PayPal settings doctype. + Click on + + top right corner +
    4. +
      +
    5. + After saving settings, + + PayPal Integration Service and Save a document. +
    6. +
      +
    7. + To view PayPal payment logs, + +
    8. +
    +
    + """ + +@frappe.whitelist(allow_guest=True, xss_safe=True) +def get_express_checkout_details(token): + doc = frappe.get_doc("PayPal Settings") + params, url = doc.get_paypal_params_and_url() + params.update({ + "METHOD": "GetExpressCheckoutDetails", + "TOKEN": token + }) + + response = doc.post_request(url, data=params) + + if response.get("ACK")[0] != "Success": + frappe.respond_as_web_page(_("Something went wrong"), + _("Looks like something went wrong during the transaction. Since we haven't confirmed the payment, Paypal will automatically refund you this amount. If it doesn't, please send us an email and mention the Correlation ID: {0}.").format(response.get("CORRELATIONID", [None])[0]), + success=False, + http_status_code=frappe.ValidationError.http_status_code) + + return + + update_integration_request_status(token, { + "payerid": response.get("PAYERID")[0], + "payer_email": response.get("EMAIL")[0] + }, "Authorized") + + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url( \ + "/api/method/frappe.integrations.paypal.confirm_payment?token={0}".format(token)) + +@frappe.whitelist(allow_guest=True, xss_safe=True) +def confirm_payment(token): + redirect = True + status_changed_to, redirect_to = None, None + doc = frappe.get_doc("PayPal Settings") + integration_request = frappe.get_doc("Integration Request", token) + data = json.loads(integration_request.data) + + redirect_to = data.get('redirect_to') or None + redirect_message = data.get('redirect_message') or None + + params, url = doc.get_paypal_params_and_url() + params.update({ + "METHOD": "DoExpressCheckoutPayment", + "PAYERID": data.get("payerid"), + "TOKEN": token, + "PAYMENTREQUEST_0_PAYMENTACTION": "SALE", + "PAYMENTREQUEST_0_AMT": data.get("amount"), + "PAYMENTREQUEST_0_CURRENCYCODE": data.get("currency").upper() + }) + + response = doc.post_request(url, data=params) + + if response.get("ACK")[0] == "Success": + update_integration_request_status(token, { + "transaction_id": response.get("PAYMENTINFO_0_TRANSACTIONID")[0], + "correlation_id": response.get("CORRELATIONID")[0] + }, "Completed") + + if data.get("reference_doctype") and data.get("reference_docname"): + redirect_url = frappe.get_doc(data.get("reference_doctype"), data.get("reference_docname")).run_method("on_payment_authorized", "Completed") + + if not redirect_url: + redirect_url = '/integrations/payment-success' + else: + redirect_url = "/integrations/payment-failed" + + if redirect_to: + redirect_url += '?' + urllib.urlencode({'redirect_to': redirect_to}) + if redirect_message: + redirect_url += '&' + urllib.urlencode({'redirect_message': redirect_message}) + + # this is done so that functions called via hooks can update flags.redirect_to + if redirect: + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url(redirect_to) + +def update_integration_request_status(token, data, status, error=False): + frappe.get_doc("Integration Request", token).update_status(data, status) + +@frappe.whitelist(allow_guest=True, xss_safe=True) +def get_checkout_url(**kwargs): + try: + doc = frappe.get_doc("PayPal Settings") + return doc.get_payment_url(**kwargs) + except Exception: + frappe.respond_as_web_page(_("Something went wrong"), + _("Looks like something is wrong with this site's Paypal configuration. Don't worry! No payment has been made from your Paypal account."), + success=False, + http_status_code=frappe.ValidationError.http_status_code) \ No newline at end of file diff --git a/frappe/integrations/doctype/razorpay_settings/__init__.py b/frappe/integrations/doctype/razorpay_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.js b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.js new file mode 100644 index 0000000000..9c61aba6af --- /dev/null +++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.js @@ -0,0 +1,33 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.provide("frappe.integration_service") + +frappe.ui.form.on('Razorpay Settings', { + refresh: function(frm) { + + } +}); + +frappe.integration_service.razorpay_settings = Class.extend({ + init: function(frm) { + + }, + + get_scheduler_job_info: function() { + return { + "Execute on every few minits of interval": " Captures all authorised payments" + } + }, + + get_service_info: function(frm) { + frappe.call({ + method: "frappe.integrations.doctype.razorpay_settings.razorpay_settings.get_service_details", + callback: function(r){ + var integration_service_help = frm.fields_dict.integration_service_help.wrapper; + $(integration_service_help).empty(); + $(integration_service_help).append(r.message); + } + }) + } +}) \ No newline at end of file diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.json b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.json new file mode 100644 index 0000000000..a0aeb0b3b7 --- /dev/null +++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.json @@ -0,0 +1,137 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-09-20 03:44:03.799402", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "editable_grid": 1, + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "api_key", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "API Key", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "api_secret", + "fieldtype": "Password", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "API Secret", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Mention transaction completion page URL", + "fieldname": "redirect_to", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Redirect To", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2016-09-26 02:00:03.604912", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Razorpay Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 1, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py new file mode 100644 index 0000000000..5437bcfd17 --- /dev/null +++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py @@ -0,0 +1,251 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import get_url, call_hook_method +from frappe import _ +import urllib, json +from frappe.integration_broker.doctype.integration_service.integration_service import IntegrationService + +""" +# Integrating RazorPay + +### Validate Currency + +Example: + + from frappe.integration_broker.doctype.integration_service.integration_service import get_integration_controller + + controller = get_integration_controller("Razorpay") + controller().validate_transaction_currency(currency) + +### 2. Redirect for payment + +Example: + + payment_details = { + "amount": 600, + "title": "Payment for bill : 111", + "description": "payment via cart", + "reference_doctype": "Payment Request", + "reference_docname": "PR0001", + "payer_email": "NuranVerkleij@example.com", + "payer_name": "Nuran Verkleij", + "order_id": "111", + "currency": "INR" + } + + # Redirect the user to this url + url = controller().get_payment_url(**payment_details) + + +### 3. On Completion of Payment + +Write a method for `on_payment_authorized` in the reference doctype + +Example: + + def on_payment_authorized(payment_status): + # this method will be called when payment is complete + + +##### Notes: + +payment_status - payment gateway will put payment status on callback. +For razorpay payment status is Authorized + +""" + +class RazorpaySettings(IntegrationService): + service_name = "Razorpay" + supported_currencies = ["INR"] + + scheduler_events = { + "all": [ + "frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment" + ] + } + + def validate(self): + if not self.flags.ignore_mandatory: + self.validate_razorpay_credentails() + + def on_update(self): + pass + + def enable(self): + call_hook_method('payment_gateway_enabled', gateway='Razorpay') + + if not self.flags.ignore_mandatory: + self.validate_razorpay_credentails() + + def validate_razorpay_credentails(self): + if self.api_key and self.api_secret: + try: + self.get_request(url="https://api.razorpay.com/v1/payments", + auth=(self.api_key, self.get_password(fieldname="api_secret", raise_exception=False))) + except Exception: + frappe.throw(_("Seems API Key or API Secret is wrong !!!")) + + def validate_transaction_currency(self, currency): + if currency not in self.supported_currencies: + frappe.throw(_("Please select another payment method. {0} does not support transactions in currency '{1}'").format(self.service_name, currency)) + + def get_payment_url(self, **kwargs): + return get_url("./integrations/razorpay_checkout?{0}".format(urllib.urlencode(kwargs))) + + def create_request(self, data): + self.data = frappe._dict(data) + + try: + self.integration_request = super(RazorpaySettings, self).create_request(self.data, "Host", \ + "Razorpay") + return self.authorize_payment() + + except Exception: + frappe.log_error(frappe.get_traceback()) + return{ + "redirect_to": frappe.redirect_to_message(_('Server Error'), _("Seems issue with server's razorpay config. Don't worry, in case of failure amount will get refunded to your account.")), + "status": 401 + } + + def authorize_payment(self): + """ + An authorization is performed when user’s payment details are successfully authenticated by the bank. + The money is deducted from the customer’s account, but will not be transferred to the merchant’s account + until it is explicitly captured by merchant. + """ + settings = self.get_settings() + data = json.loads(self.integration_request.data) + redirect_to = data.get('notes', {}).get('redirect_to') or None + redirect_message = data.get('notes', {}).get('redirect_message') or None + + try: + resp = self.get_request("https://api.razorpay.com/v1/payments/{0}" + .format(self.data.razorpay_payment_id), auth=(settings.api_key, + settings.api_secret)) + + if resp.get("status") == "authorized": + self.integration_request.db_set('status', 'Authorized', update_modified=False) + self.flags.status_changed_to = "Authorized" + + else: + frappe.log_error(str(resp), 'Razorpay Payment not authorized') + + except: + frappe.log_error(frappe.get_traceback()) + # failed + pass + + status = frappe.flags.integration_request.status_code + + if self.flags.status_changed_to == "Authorized": + if self.data.reference_doctype and self.data.reference_docname: + custom_redirect_to = None + try: + custom_redirect_to = frappe.get_doc(self.data.reference_doctype, + self.data.reference_docname).run_method("on_payment_authorized", self.flags.status_changed_to) + except Exception: + frappe.log_error(frappe.get_traceback()) + + if custom_redirect_to: + redirect_to = custom_redirect_to + + redirect_url = 'payment-success' + else: + redirect_url = 'payment-failed' + + if redirect_to: + redirect_url += '?' + urllib.urlencode({'redirect_to': redirect_to}) + if redirect_message: + redirect_url += '&' + urllib.urlencode({'redirect_message': redirect_message}) + + return { + "redirect_to": redirect_url, + "status": status + } + + def get_settings(self): + return frappe._dict({ + "api_key": self.api_key, + "api_secret": self.get_password(fieldname="api_secret", raise_exception=False) + }) + +def capture_payment(is_sandbox=False, sanbox_response=None): + """ + Verifies the purchase as complete by the merchant. + After capture, the amount is transferred to the merchant within T+3 days + where T is the day on which payment is captured. + + Note: Attempting to capture a payment whose status is not authorized will produce an error. + """ + controller = frappe.get_doc("Razorpay Settings") + + for doc in frappe.get_all("Integration Request", filters={"status": "Authorized", + "integration_request_service": "Razorpay"}, fields=["name", "data"]): + try: + if is_sandbox: + resp = sanbox_response + else: + data = json.loads(doc.data) + resp = controller.post_request("https://api.razorpay.com/v1/payments/{0}/capture".format(data.get("razorpay_payment_id")), + auth=(controller.api_key, controller.get_password("api_secret")), data={"amount": data.get("amount")}) + + if resp.get("status") == "captured": + frappe.db.set_value("Integration Request", doc.name, "status", "Completed") + + except Exception: + doc = frappe.get_doc("Integration Request", doc.name) + doc.status = "Failed" + doc.error = frappe.get_traceback() + frappe.log_error(doc.error, '{0} Failed'.format(doc.name)) + +@frappe.whitelist(allow_guest=True, xss_safe=True) +def get_checkout_url(**kwargs): + try: + return frappe.get_doc("Razorpay Settings").get_payment_url(**kwargs) + except Exception: + frappe.respond_as_web_page(_("Something went wrong"), + _("Looks like something is wrong with this site's Razorpay configuration. Don't worry! No payment has been made."), + success=False, + http_status_code=frappe.ValidationError.http_status_code) + + +@frappe.whitelist() +def get_service_details(): + return """ +
    +

    Steps to configure Service +

      +
    1. Get Razorpay api credentials by login to: + + https://razorpay.com/ + +
    2. +
      +
    3. Setup credentials on Razorpay Settings doctype. + Click on + + top right corner +
    4. +
      +
    5. + After saving settings, + + Razorpay Integration Service and Save a document. +
    6. +
      +
    7. + To view Razorpays payment logs, + +
    8. +
    +
    + """ \ No newline at end of file diff --git a/frappe/limits.py b/frappe/limits.py index 9ecb88fba0..90efaca935 100755 --- a/frappe/limits.py +++ b/frappe/limits.py @@ -4,11 +4,11 @@ from frappe import _ from frappe.utils import now_datetime, getdate, flt, cint, get_fullname from frappe.installer import update_site_config from frappe.utils.data import formatdate -from frappe.utils.user import get_enabled_system_users, get_system_managers +from frappe.utils.user import get_enabled_system_users import os, subprocess, urlparse, urllib class SiteExpiredError(frappe.ValidationError): - pass + http_status_code = 417 EXPIRY_WARNING_DAYS = 10 diff --git a/frappe/migrate.py b/frappe/migrate.py index ef88944b7e..ddc9617aff 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -12,6 +12,9 @@ from frappe.sessions import clear_global_cache from frappe.desk.notifications import clear_notifications from frappe.website import render from frappe.desk.doctype.desktop_icon.desktop_icon import sync_desktop_icons +from frappe.core.doctype.language.language import sync_languages +from frappe.modules.utils import sync_customizations +import frappe.utils.help def migrate(verbose=True, rebuild_website=False): '''Migrate all apps to the latest version, will: @@ -29,7 +32,10 @@ def migrate(verbose=True, rebuild_website=False): frappe.model.sync.sync_all(verbose=verbose) frappe.translate.clear_cache() sync_fixtures() + sync_customizations() sync_desktop_icons() + sync_languages() + frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu() # syncs statics @@ -37,6 +43,10 @@ def migrate(verbose=True, rebuild_website=False): frappe.db.commit() + if not frappe.conf.get('global_help_setup'): + # sync help if not set as global + frappe.utils.help.sync() + clear_notifications() frappe.publish_realtime("version-update") diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 3321153181..90ae393f94 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -22,6 +22,8 @@ def get_controller(doctype): :param doctype: DocType name as string.""" from frappe.model.document import Document + global _classes + if not doctype in _classes: module_name, custom = frappe.db.get_value("DocType", doctype, ["module", "custom"]) \ or ["Core", False] @@ -187,8 +189,13 @@ class BaseDocument(object): df = self.meta.get_field(fieldname) if df: - if df.fieldtype=="Check" and (not isinstance(d[fieldname], int) or d[fieldname] > 1): - d[fieldname] = 1 if cint(d[fieldname]) else 0 + if df.fieldtype=="Check": + if (not isinstance(d[fieldname], int) or d[fieldname] > 1): + d[fieldname] = 1 if cint(d[fieldname]) else 0 + + # get the default value if none, for insert / update + elif d[fieldname]==None: + d[fieldname] = df.get('default') elif df.fieldtype=="Int" and not isinstance(d[fieldname], int): d[fieldname] = cint(d[fieldname]) @@ -273,6 +280,11 @@ class BaseDocument(object): if not self.name: # name will be set by document class in most cases set_new_name(self) + + if not self.creation: + self.creation = self.modified = now() + self.created_by = self.modifield_by = frappe.session.user + d = self.get_valid_dict() columns = d.keys() try: @@ -392,8 +404,9 @@ class BaseDocument(object): return "{}: {}: {}".format(_("Error"), _("Data missing in table"), _(df.label)) elif self.parentfield: - return "{}: {} #{}: {}: {}".format(_("Error"), _("Row"), self.idx, - _("Value missing for"), _(df.label)) + + return "{}: {} {} #{}: {}: {}".format(_("Error"), frappe.bold(_(self.doctype)), + _("Row"), self.idx, _("Value missing for"), _(df.label)) else: return "{}: {}: {}".format(_("Error"), _("Value missing for"), _(df.label)) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 2c993f585d..c8c924d308 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -267,8 +267,11 @@ class DatabaseQuery(object): if not tname in self.tables: self.append_table(tname) - column_name = '{tname}.{fname}'.format(tname=tname, - fname=f.fieldname) + if 'ifnull(' in f.fieldname: + column_name = f.fieldname + else: + column_name = '{tname}.{fname}'.format(tname=tname, + fname=f.fieldname) can_be_null = True @@ -317,12 +320,10 @@ class DatabaseQuery(object): if isinstance(value, basestring): value = '"{0}"'.format(frappe.db.escape(value, percent=False)) - if f.fieldname in ("creation", "modified"): - column_name = "date_format({tname}.{fname}, '%Y-%m-%d')".format(tname=tname, - fname=f.fieldname) - - if (self.ignore_ifnull or not can_be_null - or (f.value and f.operator in ('=', 'like')) or 'ifnull(' in column_name.lower()): + if (self.ignore_ifnull + or not can_be_null + or (f.value and f.operator in ('=', 'like')) + or 'ifnull(' in column_name.lower()): condition = '{column_name} {operator} {value}'.format( column_name=column_name, operator=f.operator, value=value) diff --git a/frappe/model/db_schema.py b/frappe/model/db_schema.py index aa4d4cd04a..b6dafd701f 100644 --- a/frappe/model/db_schema.py +++ b/frappe/model/db_schema.py @@ -102,12 +102,15 @@ class DbTable: columns += self.columns.values() for col in columns: + if len(col.fieldname) >= 64: + frappe.throw(_("Fieldname is limited to 64 characters ({0})").format(frappe.bold(col.fieldname))) + if col.fieldtype in type_map and type_map[col.fieldtype][0]=="varchar": # validate length range new_length = cint(col.length) or cint(varchar_len) - if not (1 <= new_length <= 255): - frappe.throw(_("Length of {0} should be between 1 and 255").format(col.fieldname)) + if not (1 <= new_length <= 1000): + frappe.throw(_("Length of {0} should be between 1 and 1000").format(col.fieldname)) try: # check for truncation @@ -171,10 +174,11 @@ class DbTable: parenttype varchar({varchar_len}), idx int(8) not null default '0', %sindex parent(parent)) - ENGINE=InnoDB + ENGINE={engine} ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 - COLLATE=utf8mb4_unicode_ci""".format(varchar_len=varchar_len) % (self.name, add_text)) + COLLATE=utf8mb4_unicode_ci""".format(varchar_len=varchar_len, + engine=self.meta.engine or 'InnoDB') % (self.name, add_text)) def get_column_definitions(self): column_list = [] + default_columns @@ -487,13 +491,10 @@ class DbManager: if not host: host = self.get_current_host() - try: - if password: - self.db.sql("CREATE USER '%s'@'%s' IDENTIFIED BY '%s';" % (user[:16], host, password)) - else: - self.db.sql("CREATE USER '%s'@'%s';" % (user[:16], host)) - except Exception: - raise + if password: + self.db.sql("CREATE USER '%s'@'%s' IDENTIFIED BY '%s';" % (user[:16], host, password)) + else: + self.db.sql("CREATE USER '%s'@'%s';" % (user[:16], host)) def delete_user(self, target, host=None): if not host: @@ -519,7 +520,8 @@ class DbManager: if not host: host = self.get_current_host() - self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host)) + self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, + user, host)) def grant_select_privilges(self, db, table, user, host=None): if not host: diff --git a/frappe/model/document.py b/frappe/model/document.py index 0a608cb6b0..e2848af553 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -136,7 +136,7 @@ class Document(BaseDocument): self.latest = frappe.get_doc(self.doctype, self.name) return self.latest - def check_permission(self, permtype, permlabel=None): + def check_permission(self, permtype='read', permlabel=None): """Raise `frappe.PermissionError` if not permitted""" if not self.has_permission(permtype): self.raise_no_permission_to(permlabel or permtype) @@ -150,17 +150,6 @@ class Document(BaseDocument): return True return frappe.has_permission(self.doctype, permtype, self, verbose=verbose) - def has_website_permission(self, permtype="read", verbose=False): - """Call `frappe.has_website_permission` if `self.flags.ignore_permissions` - is not set. - - :param permtype: one of `read`, `write`, `submit`, `cancel`, `delete`""" - if self.flags.ignore_permissions: - return True - - return (frappe.has_website_permission(self.doctype, permtype, self, verbose=verbose) - or self.has_permission(permtype, verbose=verbose)) - def raise_no_permission_to(self, perm_type): """Raise `frappe.PermissionError`.""" msg = _("No permission to {0} {1} {2}".format(perm_type, self.doctype, self.name or "")) @@ -193,6 +182,8 @@ class Document(BaseDocument): if self.flags.in_print: return + self.flags.email_alerts_executed = [] + if ignore_permissions!=None: self.flags.ignore_permissions = ignore_permissions @@ -252,6 +243,8 @@ class Document(BaseDocument): if self.flags.in_print: return + self.flags.email_alerts_executed = [] + if ignore_permissions!=None: self.flags.ignore_permissions = ignore_permissions @@ -658,7 +651,57 @@ class Document(BaseDocument): fn = lambda self, *args, **kwargs: None fn.__name__ = method.encode("utf-8") - return Document.hook(fn)(self, *args, **kwargs) + out = Document.hook(fn)(self, *args, **kwargs) + + self.run_email_alerts(method) + + return out + + def run_trigger(self, method, *args, **kwargs): + return self.run_method(method, *args, **kwargs) + + def run_email_alerts(self, method): + '''Run email alerts for this method''' + if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install: + return + + if self.flags.email_alerts_executed==None: + self.flags.email_alerts_executed = [] + + from frappe.email.doctype.email_alert.email_alert import evaluate_alert + + if self.flags.email_alerts == None: + alerts = frappe.cache().hget('email_alerts', self.doctype) + if alerts==None: + alerts = frappe.get_all('Email Alert', fields=['name', 'event', 'method'], + filters={'enabled': 1, 'document_type': self.doctype}) + frappe.cache().hset('email_alerts', self.doctype, alerts) + self.flags.email_alerts = alerts + + if not self.flags.email_alerts: + return + + def _evaluate_alert(alert): + if not alert.name in self.flags.email_alerts_executed: + evaluate_alert(self, alert.name, alert.event) + + event_map = { + "on_update": "Save", + "after_insert": "New", + "on_submit": "Submit", + "on_cancel": "Cancel" + } + + if not self.flags.in_insert: + # value change is not applicable in insert + event_map['validate'] = 'Value Change' + + for alert in self.flags.email_alerts: + event = event_map.get(method, None) + if event and alert.event == event: + _evaluate_alert(alert) + elif alert.event=='Method' and method == alert.method: + _evaluate_alert(alert) @staticmethod def whitelist(f): @@ -1000,12 +1043,12 @@ def execute_action(doctype, name, action, **kwargs): getattr(doc, action)(**kwargs) except Exception: frappe.db.rollback() - + # add a comment (?) if frappe.local.message_log: msg = json.loads(frappe.local.message_log[-1]).get('message') else: msg = '
    ' + frappe.get_traceback() + '
    ' - + doc.add_comment('Comment', _('Action Failed') + '

    ' + msg) doc.notify_update() diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 012f99d94a..3435126147 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -23,6 +23,7 @@ from frappe.model.document import Document from frappe.model.base_document import BaseDocument from frappe.model.db_schema import type_map from frappe.modules import load_doctype_module +from frappe import _ def get_meta(doctype, cached=True): if cached: @@ -117,7 +118,19 @@ class Meta(Document): return True if self.get_field(fieldname) else False def get_label(self, fieldname): - return self.get_field(fieldname).label + '''Get label of the given fieldname''' + df = self.get_field(fieldname) + if df: + label = df.label + else: + label = { + 'name': _('ID'), + 'owner': _('Created By'), + 'modified_by': _('Modified By'), + 'creation': _('Created On'), + 'modified': _('Last Modified On') + }.get(fieldname) or _('No Label') + return label def get_options(self, fieldname): return self.get_field(fieldname).options @@ -268,11 +281,13 @@ class Meta(Document): This method will return the `data` property in the `[doctype]_dashboard.py` file in the doctype folder''' + data = frappe._dict() try: module = load_doctype_module(self.name, suffix='_dashboard') - data = frappe._dict(module.get_data()) + if hasattr(module, 'get_data'): + data = frappe._dict(module.get_data()) except ImportError: - data = frappe._dict() + pass return data @@ -402,7 +417,8 @@ def clear_cache(doctype=None): for key in ('is_table', 'doctype_modules'): cache.delete_value(key) - groups = ["meta", "form_meta", "table_columns", "last_modified", "linked_doctypes"] + groups = ["meta", "form_meta", "table_columns", "last_modified", + "linked_doctypes", 'email_alerts'] def clear_single(dt): for name in groups: diff --git a/frappe/model/naming.py b/frappe/model/naming.py index b7828bb49e..5f65a8b3f3 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -56,7 +56,7 @@ def set_new_name(doc): if not doc.name or autoname=='hash': doc.name = make_autoname('hash', doc.doctype) - doc.name = validate_name(doc.doctype, doc.name) + doc.name = validate_name(doc.doctype, doc.name, frappe.get_meta(doc.doctype).get_field("name_case")) def set_name_by_naming_series(doc): """Sets name by the `naming_series` property""" diff --git a/frappe/model/sync.py b/frappe/model/sync.py index ca3e1a3428..2253778f50 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -29,7 +29,8 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False): # these need to go first at time of install for d in (("core", "docfield"), ("core", "docperm"), ("core", "doctype"), ("core", "user"), ("core", "role"), ("custom", "custom_field"), - ("custom", "property_setter")): + ("custom", "property_setter"), ("website", "web_form"), + ("website", "web_form_field"), ("website", "portal_menu_item")): files.append(os.path.join(frappe.get_app_path("frappe"), d[0], "doctype", d[1], d[1] + ".json")) @@ -54,7 +55,7 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False): def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=False): """walk and sync all doctypes and pages""" - document_type = ['doctype', 'page', 'report', 'print_format', 'website_theme', 'web_form'] + document_type = ['doctype', 'page', 'report', 'print_format', 'website_theme', 'web_form', 'email_alert'] for doctype in document_type: doctype_path = os.path.join(start_path, doctype) if os.path.exists(doctype_path): diff --git a/frappe/modules.txt b/frappe/modules.txt index 1b8d17eb40..8a86566c04 100644 --- a/frappe/modules.txt +++ b/frappe/modules.txt @@ -6,4 +6,5 @@ Custom Geo Desk Print -Integrations \ No newline at end of file +Integrations +Integration Broker \ No newline at end of file diff --git a/frappe/modules/__init__.py b/frappe/modules/__init__.py index d271efa8fb..90f60fdd89 100644 --- a/frappe/modules/__init__.py +++ b/frappe/modules/__init__.py @@ -1,103 +1 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals -""" - Utilities for using modules -""" -import frappe, os -import frappe.utils -from frappe import _ - -lower_case_files_for = ['DocType', 'Page', 'Report', - "Workflow", 'Module Def', 'Desktop Item', 'Workflow State', 'Workflow Action', 'Print Format', - "Website Theme", 'Web Form'] - -def scrub(txt): - return frappe.scrub(txt) - -def scrub_dt_dn(dt, dn): - """Returns in lowercase and code friendly names of doctype and name for certain types""" - ndt, ndn = dt, dn - if dt in lower_case_files_for: - ndt, ndn = scrub(dt), scrub(dn) - - return ndt, ndn - -def get_module_path(module): - """Returns path of the given module""" - return frappe.get_module_path(module) - -def get_doc_path(module, doctype, name): - dt, dn = scrub_dt_dn(doctype, name) - return os.path.join(get_module_path(module), dt, dn) - -def reload_doc(module, dt=None, dn=None, force=True): - from frappe.modules.import_file import import_files - return import_files(module, dt, dn, force=force) - -def export_doc(doctype, name, module=None): - """Write a doc to standard path.""" - from frappe.modules.export_file import write_document_file - print doctype, name - - if not module: module = frappe.db.get_value('DocType', name, 'module') - write_document_file(frappe.get_doc(doctype, name), module) - -def get_doctype_module(doctype): - """Returns **Module Def** name of given doctype.""" - def make_modules_dict(): - return dict(frappe.db.sql("select name, module from tabDocType")) - return frappe.cache().get_value("doctype_modules", make_modules_dict)[doctype] - -doctype_python_modules = {} -def load_doctype_module(doctype, module=None, prefix="", suffix=""): - """Returns the module object for given doctype.""" - if not module: - module = get_doctype_module(doctype) - - app = get_module_app(module) - - key = (app, doctype, prefix, suffix) - - if key not in doctype_python_modules: - doctype_python_modules[key] = frappe.get_module(get_module_name(doctype, module, prefix, suffix)) - - return doctype_python_modules[key] - -def get_module_name(doctype, module, prefix="", suffix="", app=None): - return '{app}.{module}.doctype.{doctype}.{prefix}{doctype}{suffix}'.format(\ - app = scrub(app or get_module_app(module)), - module = scrub(module), - doctype = scrub(doctype), - prefix=prefix, - suffix=suffix) - -def get_module_app(module): - return frappe.local.module_app[scrub(module)] - -def get_app_publisher(module): - app = frappe.local.module_app[scrub(module)] - if not app: - frappe.throw(_("App not found")) - app_publisher = frappe.get_hooks(hook="app_publisher", app_name=app)[0] - return app_publisher - -def make_boilerplate(template, doc, opts=None): - target_path = get_doc_path(doc.module, doc.doctype, doc.name) - template_name = template.replace("controller", scrub(doc.name)) - target_file_path = os.path.join(target_path, template_name) - - app_publisher = get_app_publisher(doc.module) - - if not os.path.exists(target_file_path): - if not opts: - opts = {} - - with open(target_file_path, 'w') as target: - with open(os.path.join(get_module_path("core"), "doctype", scrub(doc.doctype), - "boilerplate", template), 'r') as source: - target.write(frappe.utils.encode( - frappe.utils.cstr(source.read()).format(app_publisher=app_publisher, - classname=doc.name.replace(" ", ""), doctype=doc.name, **opts) - )) +from .utils import * \ No newline at end of file diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py index a499133163..3bade9c3f5 100644 --- a/frappe/modules/patch_handler.py +++ b/frappe/modules/patch_handler.py @@ -14,6 +14,9 @@ from __future__ import unicode_literals """ import frappe, frappe.permissions +# for patches +import os + class PatchError(Exception): pass def run_all(): diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py new file mode 100644 index 0000000000..35ff2b4691 --- /dev/null +++ b/frappe/modules/utils.py @@ -0,0 +1,213 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +""" + Utilities for using modules +""" +import frappe, os, json +import frappe.utils +from frappe import _ + +lower_case_files_for = ['DocType', 'Page', 'Report', + "Workflow", 'Module Def', 'Desktop Item', 'Workflow State', 'Workflow Action', 'Print Format', + "Website Theme", 'Web Form', 'Email Alert'] + +def export_module_json(doc, is_standard, module): + """Make a folder for the given doc and add its json file (make it a standard + object that will be synced)""" + if (not frappe.flags.in_import and getattr(frappe.get_conf(),'developer_mode', 0) + and is_standard): + from frappe.modules.export_file import export_to_files + from frappe.modules import get_module_path + + # json + export_to_files(record_list=[[doc.doctype, doc.name]], record_module=module) + + path = os.path.join(get_module_path(module), scrub(doc.doctype), + scrub(doc.name), scrub(doc.name)) + + return path + +def get_doc_module(module, doctype, name): + """Get custom module for given document""" + module_name = "{app}.{module}.{doctype}.{name}.{name}".format( + app = frappe.local.module_app[scrub(module)], + doctype = scrub(doctype), + module = scrub(module), + name = scrub(name) + ) + return frappe.get_module(module_name) + +@frappe.whitelist() +def export_customizations(module, doctype, sync_on_migrate=0): + """Export Custom Field and Property Setter for the current document to the app folder. + This will be synced with bench migrate""" + if not frappe.get_conf().developer_mode: + raise 'Not developer mode' + + custom = {'custom_fields': [], 'property_setters': [], + 'doctype': doctype, 'sync_on_migrate': 1} + + def add(_doctype): + custom['custom_fields'] += frappe.get_all('Custom Field', + fields='*', filters={'dt': _doctype}) + custom['property_setters'] += frappe.get_all('Property Setter', + fields='*', filters={'doc_type': _doctype}) + + add(doctype) + + # add custom fields and property setters for all child tables + for d in frappe.get_meta(doctype).get_table_fields(): + add(d.options) + + folder_path = os.path.join(get_module_path(module), 'custom') + if not os.path.exists(folder_path): + os.makedirs(folder_path) + + path = os.path.join(folder_path, scrub(doctype)+ '.json') + with open(path, 'w') as f: + f.write(frappe.as_json(custom)) + + frappe.msgprint('Customizations exported to {0}'.format(path)) + +def sync_customizations(app=None): + '''Sync custom fields and property setters from custom folder in each app module''' + + if app: + apps = [app] + else: + apps = frappe.get_installed_apps() + + for app_name in apps: + for module_name in frappe.local.app_modules.get(app_name) or []: + folder = frappe.get_app_path(app_name, module_name, 'custom') + + if os.path.exists(folder): + for fname in os.listdir(folder): + with open(os.path.join(folder, fname), 'r') as f: + data = json.loads(f.read()) + + if data.get('sync_on_migrate'): + sync_customizations_for_doctype(data) + + +def sync_customizations_for_doctype(data): + '''Sync doctype customzations for a particular data set''' + from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype + + doctype = data['doctype'] + if data['custom_fields']: + frappe.db.sql('delete from `tabCustom Field` where dt=%s', doctype) + + for d in data['custom_fields']: + d['doctype'] = 'Custom Field' + doc = frappe.get_doc(d) + doc.db_insert() + + if data['property_setters']: + frappe.db.sql('delete from `tabProperty Setter` where doc_type=%s', doctype) + + for d in data['property_setters']: + d['doctype'] = 'Property Setter' + doc = frappe.get_doc(d) + doc.db_insert() + + print 'Updating customizations for {0}'.format(doctype) + validate_fields_for_doctype(doctype) + + +def scrub(txt): + return frappe.scrub(txt) + +def scrub_dt_dn(dt, dn): + """Returns in lowercase and code friendly names of doctype and name for certain types""" + ndt, ndn = dt, dn + if dt in lower_case_files_for: + ndt, ndn = scrub(dt), scrub(dn) + + return ndt, ndn + +def get_module_path(module): + """Returns path of the given module""" + return frappe.get_module_path(module) + +def get_doc_path(module, doctype, name): + dt, dn = scrub_dt_dn(doctype, name) + return os.path.join(get_module_path(module), dt, dn) + +def reload_doc(module, dt=None, dn=None, force=True): + from frappe.modules.import_file import import_files + return import_files(module, dt, dn, force=force) + +def export_doc(doctype, name, module=None): + """Write a doc to standard path.""" + from frappe.modules.export_file import write_document_file + print doctype, name + + if not module: module = frappe.db.get_value('DocType', name, 'module') + write_document_file(frappe.get_doc(doctype, name), module) + +def get_doctype_module(doctype): + """Returns **Module Def** name of given doctype.""" + def make_modules_dict(): + return dict(frappe.db.sql("select name, module from tabDocType")) + return frappe.cache().get_value("doctype_modules", make_modules_dict)[doctype] + +doctype_python_modules = {} +def load_doctype_module(doctype, module=None, prefix="", suffix=""): + """Returns the module object for given doctype.""" + if not module: + module = get_doctype_module(doctype) + + app = get_module_app(module) + + key = (app, doctype, prefix, suffix) + + module_name = get_module_name(doctype, module, prefix, suffix) + + try: + if key not in doctype_python_modules: + doctype_python_modules[key] = frappe.get_module(module_name) + except ImportError: + print 'Module import failed for {0} ({1})'.format(doctype, module_name) + raise + + return doctype_python_modules[key] + +def get_module_name(doctype, module, prefix="", suffix="", app=None): + return '{app}.{module}.doctype.{doctype}.{prefix}{doctype}{suffix}'.format(\ + app = scrub(app or get_module_app(module)), + module = scrub(module), + doctype = scrub(doctype), + prefix=prefix, + suffix=suffix) + +def get_module_app(module): + return frappe.local.module_app[scrub(module)] + +def get_app_publisher(module): + app = frappe.local.module_app[scrub(module)] + if not app: + frappe.throw(_("App not found")) + app_publisher = frappe.get_hooks(hook="app_publisher", app_name=app)[0] + return app_publisher + +def make_boilerplate(template, doc, opts=None): + target_path = get_doc_path(doc.module, doc.doctype, doc.name) + template_name = template.replace("controller", scrub(doc.name)) + target_file_path = os.path.join(target_path, template_name) + + app_publisher = get_app_publisher(doc.module) + + if not os.path.exists(target_file_path): + if not opts: + opts = {} + + with open(target_file_path, 'w') as target: + with open(os.path.join(get_module_path("core"), "doctype", scrub(doc.doctype), + "boilerplate", template), 'r') as source: + target.write(frappe.utils.encode( + frappe.utils.cstr(source.read()).format(app_publisher=app_publisher, + classname=doc.name.replace(" ", ""), doctype=doc.name, **opts) + )) diff --git a/frappe/oauth.py b/frappe/oauth.py new file mode 100644 index 0000000000..22e1f17562 --- /dev/null +++ b/frappe/oauth.py @@ -0,0 +1,282 @@ +import frappe, urllib + +from urlparse import parse_qs, urlparse +from oauthlib.oauth2.rfc6749.tokens import BearerToken +from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant +from oauthlib.oauth2 import RequestValidator +from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint +from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint +from oauthlib.oauth2.rfc6749.endpoints.resource import ResourceEndpoint +from oauthlib.oauth2.rfc6749.endpoints.revocation import RevocationEndpoint +from oauthlib.common import Request + +class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, + RevocationEndpoint): + + """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" + + def __init__(self, request_validator, token_generator=None, + token_expires_in=None, refresh_token_generator=None, **kwargs): + """Construct a new web application server. + + :param request_validator: An implementation of + oauthlib.oauth2.RequestValidator. + :param token_expires_in: An int or a function to generate a token + expiration offset (in seconds) given a + oauthlib.common.Request object. + :param token_generator: A function to generate a token from a request. + :param refresh_token_generator: A function to generate a token from a + request for the refresh token. + :param kwargs: Extra parameters to pass to authorization-, + token-, resource-, and revocation-endpoint constructors. + """ + auth_grant = AuthorizationCodeGrant(request_validator) + refresh_grant = RefreshTokenGrant(request_validator) + bearer = BearerToken(request_validator, token_generator, + token_expires_in, refresh_token_generator) + AuthorizationEndpoint.__init__(self, default_response_type='code', + response_types={'code': auth_grant}, + default_token_type=bearer) + TokenEndpoint.__init__(self, default_grant_type='authorization_code', + grant_types={ + 'authorization_code': auth_grant, + 'refresh_token': refresh_grant, + }, + default_token_type=bearer) + ResourceEndpoint.__init__(self, default_token='Bearer', + token_types={'Bearer': bearer}) + RevocationEndpoint.__init__(self, request_validator) + + +class OAuthWebRequestValidator(RequestValidator): + + # Pre- and post-authorization. + def validate_client_id(self, client_id, request, *args, **kwargs): + # Simple validity check, does client exist? Not banned? + cli_id = frappe.db.get_value("OAuth Client",{ "name":client_id }) + if cli_id: + request.client = frappe.get_doc("OAuth Client", client_id).as_dict() + return True + else: + return False + + def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs): + # Is the client allowed to use the supplied redirect_uri? i.e. has + # the client previously registered this EXACT redirect uri. + + redirect_uris = frappe.db.get_value("OAuth Client", client_id, 'redirect_uris').split(';') + + if redirect_uri in redirect_uris: + return True + else: + return False + + def get_default_redirect_uri(self, client_id, request, *args, **kwargs): + # The redirect used if none has been supplied. + # Prefer your clients to pre register a redirect uri rather than + # supplying one on each authorization request. + redirect_uri = frappe.db.get_value("OAuth Client", client_id, 'default_redirect_uri') + return redirect_uri + + def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): + # Is the client allowed to access the requested scopes? + client_scopes = frappe.db.get_value("OAuth Client", client_id, 'scopes').split(';') + + are_scopes_valid = True + + for scp in scopes: + are_scopes_valid = are_scopes_valid and True if scp in client_scopes else False + + return are_scopes_valid + + def get_default_scopes(self, client_id, request, *args, **kwargs): + # Scopes a client will authorize for if none are supplied in the + # authorization request. + scopes = frappe.db.get_value("OAuth Client", client_id, 'scopes').split(';') + request.scopes = scopes #Apparently this is possible. + return scopes + + def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs): + # Clients should only be allowed to use one type of response type, the + # one associated with their one allowed grant type. + # In this case it must be "code". + + return (client.response_type.lower() == response_type) + + + # Post-authorization + + def save_authorization_code(self, client_id, code, request, *args, **kwargs): + + cookie_dict = get_cookie_dict_from_headers(request) + + oac = frappe.new_doc('OAuth Authorization Code') + oac.scopes = ';'.join(request.scopes) + oac.redirect_uri_bound_to_authorization_code = request.redirect_uri + oac.client = client_id + oac.user = urllib.unquote(cookie_dict['user_id']) + oac.authorization_code = code['code'] + oac.save(ignore_permissions=True) + frappe.db.commit() + + def authenticate_client(self, request, *args, **kwargs): + + cookie_dict = get_cookie_dict_from_headers(request) + + #Get ClientID in URL + if request.client_id: + oc = frappe.get_doc("OAuth Client", request.client_id) + else: + #Extract token, instantiate OAuth Bearer Token and use clientid from there. + if frappe.form_dict.has_key("refresh_token"): + oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", {"refresh_token": frappe.form_dict["refresh_token"]}, 'client')) + else: + oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", frappe.form_dict["token"], 'client')) + try: + request.client = request.client or oc.as_dict() + except Exception, e: + print "Failed body authentication: Application %s does not exist".format(cid=request.client_id) + + return frappe.session.user == urllib.unquote(cookie_dict.get('user_id', "Guest")) + + def authenticate_client_id(self, client_id, request, *args, **kwargs): + cli_id = frappe.db.get_value('OAuth Client', client_id, 'name') + if not cli_id: + # Don't allow public (non-authenticated) clients + return False + else: + request["client"] = frappe.get_doc("OAuth Client", cli_id) + return True + + def validate_code(self, client_id, code, client, request, *args, **kwargs): + # Validate the code belongs to the client. Add associated scopes, + # state and user to request.scopes and request.user. + + validcodes = frappe.get_all("OAuth Authorization Code", filters={"client": client_id, "validity": "Valid"}) + + checkcodes = [] + for vcode in validcodes: + checkcodes.append(vcode["name"]) + + if code in checkcodes: + request.scopes = frappe.db.get_value("OAuth Authorization Code", code, 'scopes').split(';') + request.user = frappe.db.get_value("OAuth Authorization Code", code, 'user') + return True + else: + return False + + def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **kwargs): + saved_redirect_uri = frappe.db.get_value('OAuth Client', client_id, 'default_redirect_uri') + + return saved_redirect_uri == redirect_uri + + def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs): + # Clients should only be allowed to use one type of grant. + # In this case, it must be "authorization_code" or "refresh_token" + return (grant_type in ["authorization_code", "refresh_token"]) + + def save_bearer_token(self, token, request, *args, **kwargs): + # Remember to associate it with request.scopes, request.user and + # request.client. The two former will be set when you validate + # the authorization code. Don't forget to save both the + # access_token and the refresh_token and set expiration for the + # access_token to now + expires_in seconds. + + otoken = frappe.new_doc("OAuth Bearer Token") + otoken.client = request.client['name'] + otoken.user = request.user + otoken.scopes = ";".join(request.scopes) + otoken.access_token = token['access_token'] + otoken.refresh_token = token['refresh_token'] + otoken.expires_in = token['expires_in'] + otoken.save(ignore_permissions=True) + frappe.db.commit() + + default_redirect_uri = frappe.db.get_value("OAuth Client", request.client['name'], "default_redirect_uri") + return default_redirect_uri + + def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs): + # Authorization codes are use once, invalidate it when a Bearer token + # has been acquired. + + frappe.db.set_value("OAuth Authorization Code", code, "validity", "Invalid") + frappe.db.commit() + + # Protected resource request + + def validate_bearer_token(self, token, scopes, request): + # Remember to check expiration and scope membership + otoken = frappe.get_doc("OAuth Bearer Token", token) #{"access_token": str(token)}) + is_token_valid = (frappe.utils.datetime.datetime.now() < otoken.expiration_time) \ + and otoken.status != "Revoked" + client_scopes = frappe.db.get_value("OAuth Client", otoken.client, 'scopes').split(';') + are_scopes_valid = True + for scp in scopes: + are_scopes_valid = are_scopes_valid and True if scp in client_scopes else False + + return is_token_valid and are_scopes_valid + + # Token refresh request + def get_original_scopes(self, refresh_token, request, *args, **kwargs): + # Obtain the token associated with the given refresh_token and + # return its scopes, these will be passed on to the refreshed + # access token if the client did not specify a scope during the + # request. + obearer_token = frappe.get_doc("OAuth Bearer Token", {"refresh_token": refresh_token}) + return obearer_token.scopes + + def revoke_token(self, token, token_type_hint, request, *args, **kwargs): + """Revoke an access or refresh token. + + :param token: The token string. + :param token_type_hint: access_token or refresh_token. + :param request: The HTTP Request (oauthlib.common.Request) + + Method is used by: + - Revocation Endpoint + """ + otoken = None + + if token_type_hint == "access_token": + otoken = frappe.db.set_value("OAuth Bearer Token", token, 'status', 'Revoked') + elif token_type_hint == "refresh_token": + otoken = frappe.db.set_value("OAuth Bearer Token", {"refresh_token": token}, 'status', 'Revoked') + else: + otoken = frappe.db.set_value("OAuth Bearer Token", token, 'status', 'Revoked') + frappe.db.commit() + + def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs): + # """Ensure the Bearer token is valid and authorized access to scopes. + + # OBS! The request.user attribute should be set to the resource owner + # associated with this refresh token. + + # :param refresh_token: Unicode refresh token + # :param client: Client object set by you, see authenticate_client. + # :param request: The HTTP Request (oauthlib.common.Request) + # :rtype: True or False + + # Method is used by: + # - Authorization Code Grant (indirectly by issuing refresh tokens) + # - Resource Owner Password Credentials Grant (also indirectly) + # - Refresh Token Grant + # """ + # raise NotImplementedError('Subclasses must implement this method.') + + otoken = frappe.get_doc("OAuth Bearer Token", {"refresh_token": refresh_token, "status": "Active"}) + + if not otoken: + return False + else: + return True + + #TODO: Validate scopes. + +def get_cookie_dict_from_headers(r): + if r.headers.get('Cookie'): + cookie = r.headers.get('Cookie') + cookie = cookie.split("; ") + cookie_dict = {k:v for k,v in (x.split('=') for x in cookie)} + return cookie_dict + else: + return {} diff --git a/frappe/patches.txt b/frappe/patches.txt index e20a130d0f..2a4ad37f81 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -1,12 +1,14 @@ execute:frappe.db.sql("""update `tabPatch Log` set patch=replace(patch, '.4_0.', '.v4_0.')""") #2014-05-12 +frappe.patches.v7_1.rename_scheduler_log_to_error_log frappe.patches.v5_0.convert_to_barracuda_and_utf8mb4 frappe.patches.v7_0.update_auth frappe.patches.v6_1.rename_file_data frappe.patches.v7_0.re_route #2016-06-27 -execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2016-07-11 +execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2016-10-17 execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2016-02-26 execute:frappe.reload_doc('core', 'doctype', 'docperm') #2014-06-24 execute:frappe.reload_doc('core', 'doctype', 'role') +execute:frappe.reload_doc('core', 'doctype', 'user') execute:frappe.reload_doc('custom', 'doctype', 'custom_field') #2015-10-19 execute:frappe.reload_doc('core', 'doctype', 'page') #2013-13-26 execute:frappe.reload_doc('core', 'doctype', 'report') #2014-06-03 @@ -18,7 +20,10 @@ execute:frappe.reload_doc('custom', 'doctype', 'property_setter') #2014-12-31-1 execute:frappe.reload_doc('core', 'doctype', 'patch_log') #2016-10-31 execute:frappe.reload_doctype("File") # 2015-10-19 execute:frappe.reload_doc('core', 'doctype', 'error_snapshot') +execute:frappe.clear_cache() +frappe.patches.v7_1.sync_language_doctype frappe.patches.v7_0.rename_bulk_email_to_email_queue +frappe.patches.v7_1.rename_chinese_language_codes execute:frappe.db.sql("alter table `tabSessions` modify `user` varchar(255), engine=InnoDB") execute:frappe.db.sql("delete from `tabDocField` where parent='0'") @@ -98,7 +103,6 @@ frappe.patches.v5_0.modify_session frappe.patches.v5_0.expire_old_scheduler_logs execute:frappe.permissions.reset_perms("DocType") execute:frappe.db.sql("delete from `tabProperty Setter` where `property` = 'idx'") -frappe.patches.v5_3.rename_chinese_languages frappe.patches.v6_0.communication_status_and_permission frappe.patches.v6_0.make_task_log_folder frappe.patches.v6_0.document_type_rename @@ -107,13 +111,10 @@ frappe.patches.v6_2.ignore_user_permissions_if_missing execute:frappe.db.sql("delete from tabSessions where user is null") frappe.patches.v6_2.rename_backup_manager execute:frappe.delete_doc("DocType", "Backup Manager") -frappe.patches.v6_4.rename_bengali_language execute:frappe.db.sql("""update `tabCommunication` set parenttype=null, parent=null, parentfield=null""") #2015-10-22 execute:frappe.permissions.reset_perms("Web Page") frappe.patches.v6_6.user_last_active -frappe.patches.v6_6.rename_slovak_language frappe.patches.v6_6.fix_file_url -frappe.patches.v6_9.rename_burmese_language frappe.patches.v6_11.rename_field_in_email_account frappe.patches.v7_0.create_private_file_folder frappe.patches.v6_15.remove_property_setter_for_previous_field #2015-12-29 @@ -136,5 +137,10 @@ execute:frappe.db.sql("delete from `tabWeb Page` where ifnull(template_path, '') frappe.patches.v7_0.rename_newsletter_list_to_email_group frappe.patches.v7_0.replace_upgrade_link_limit frappe.patches.v7_0.set_email_group +frappe.patches.v7_1.setup_integration_services #2016-09-16 +frappe.patches.v7_1.rename_chinese_language_codes +execute:frappe.core.doctype.language.language.update_language_names() execute:frappe.db.set_value("Print Settings", "Print Settings", "add_draft_heading", 1) frappe.patches.v7_0.cleanup_list_settings +execute:frappe.db.set_default('language', '') +frappe.patches.v7_1.refactor_integration_broker diff --git a/frappe/patches/v5_0/expire_old_scheduler_logs.py b/frappe/patches/v5_0/expire_old_scheduler_logs.py index 3c4b12a356..2d107d26f6 100644 --- a/frappe/patches/v5_0/expire_old_scheduler_logs.py +++ b/frappe/patches/v5_0/expire_old_scheduler_logs.py @@ -1,7 +1,7 @@ import frappe def execute(): - frappe.reload_doctype("Scheduler Log") + frappe.reload_doctype("Error Log") - from frappe.core.doctype.scheduler_log.scheduler_log import set_old_logs_as_seen + from frappe.core.doctype.error_log.error_log import set_old_logs_as_seen set_old_logs_as_seen() diff --git a/frappe/patches/v5_0/update_shared.py b/frappe/patches/v5_0/update_shared.py index 8ce3f842af..4ba908b334 100644 --- a/frappe/patches/v5_0/update_shared.py +++ b/frappe/patches/v5_0/update_shared.py @@ -13,7 +13,7 @@ def execute(): users = frappe.get_all("User", filters={"user_type": "System User"}) usernames = [user.name for user in users] for user in usernames: - frappe.share.add("User", user, user, share=1) + frappe.share.add("User", user, user, write=1, share=1) # move event user to shared if frappe.db.exists("DocType", "Event User"): diff --git a/frappe/patches/v6_24/set_language_as_code.py b/frappe/patches/v6_24/set_language_as_code.py index 2883120551..0679459783 100644 --- a/frappe/patches/v6_24/set_language_as_code.py +++ b/frappe/patches/v6_24/set_language_as_code.py @@ -4,15 +4,4 @@ from frappe.translate import get_lang_dict # migrate language from name to code def execute(): - language = frappe.db.get_value('System Settings', None, 'language') - if language: - system_settings = frappe.get_doc('System Settings', 'System Settings') - if get_lang_dict().get(language, language) != system_settings.language: - system_settings.language = get_lang_dict().get(language, language) - system_settings.flags.ignore_mandatory=True - system_settings.save() - - for user in frappe.get_all('User', fields=['name', 'language']): - if user.language: - frappe.db.set_value('User', user.name, 'language', - get_lang_dict().get(user.language, user.language), update_modified=False) + return diff --git a/frappe/patches/v7_0/desktop_icons_hidden_by_admin_as_blocked.py b/frappe/patches/v7_0/desktop_icons_hidden_by_admin_as_blocked.py index e3f8f7b325..269a8978d4 100644 --- a/frappe/patches/v7_0/desktop_icons_hidden_by_admin_as_blocked.py +++ b/frappe/patches/v7_0/desktop_icons_hidden_by_admin_as_blocked.py @@ -6,4 +6,6 @@ def execute(): # in 7.0, icons may be hidden by default, but still can be shown to the user # e.g. Accounts, Stock etc, so we need a new property for blocked - frappe.db.sql('update `tabDesktop Icon` set blocked = 1 where standard=1 and hidden=1') \ No newline at end of file + + if frappe.db.table_exists('Desktop Icon'): + frappe.db.sql('update `tabDesktop Icon` set blocked = 1 where standard=1 and hidden=1') \ No newline at end of file diff --git a/frappe/patches/v7_1/__init__.py b/frappe/patches/v7_1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/patches/v7_1/refactor_integration_broker.py b/frappe/patches/v7_1/refactor_integration_broker.py new file mode 100644 index 0000000000..8c9aaa6795 --- /dev/null +++ b/frappe/patches/v7_1/refactor_integration_broker.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +import json + +def execute(): + for doctype_name in ["Razorpay Log", "Razorpay Payment", "Razorpay Settings"]: + delete_doc("DocType", doctype_name) + + reload_doctypes() + setup_services() + +def delete_doc(doctype, doctype_name): + frappe.delete_doc(doctype, doctype_name) + +def reload_doctypes(): + for doctype in ("razorpay_settings", "paypal_settings", "dropbox_settings", "ldap_settings"): + frappe.reload_doc("integrations", "doctype", doctype) + +def setup_services(): + for service in [{"old_name": "Razorpay", "new_name": "Razorpay"}, + {"old_name": "PayPal", "new_name": "PayPal"}, + {"old_name": "Dropbox Integration", "new_name": "Dropbox"}, + {"old_name": "LDAP Auth", "new_name": "LDAP"}]: + + try: + service_doc = frappe.get_doc("Integration Service", service["old_name"]) + settings = json.loads(service_doc.custom_settings_json) + + service_settings = frappe.new_doc("{0} Settings".format(service["new_name"])) + service_settings.update(settings) + + service_settings.flags.ignore_mandatory = True + service_settings.save(ignore_permissions=True) + + if service["old_name"] in ["Dropbox Integration", "LDAP Auth"]: + delete_doc("Integration Service", service["old_name"]) + + new_service_doc = frappe.get_doc({ + "doctype": "Integration Service", + "service": service["new_name"], + "enabled": 1 + }) + + new_service_doc.flags.ignore_mandatory = True + new_service_doc.save(ignore_permissions=True) + + except Exception: + pass diff --git a/frappe/patches/v7_1/rename_chinese_language_codes.py b/frappe/patches/v7_1/rename_chinese_language_codes.py new file mode 100644 index 0000000000..858246faa2 --- /dev/null +++ b/frappe/patches/v7_1/rename_chinese_language_codes.py @@ -0,0 +1,10 @@ +import frappe + +def execute(): + frappe.rename_doc('Language', 'zh-cn', 'zh', force=True, + merge=True if frappe.db.exists('Language', 'zh') else False) + if frappe.db.get_value('Language', 'zh-tw') == 'zh-tw': + frappe.rename_doc('Language', 'zh-tw', 'zh-TW', force=True) + + frappe.db.set_value('Language', 'zh', 'language_code', 'zh') + frappe.db.set_value('Language', 'zh-TW', 'language_code', 'zh-TW') \ No newline at end of file diff --git a/frappe/patches/v7_1/rename_scheduler_log_to_error_log.py b/frappe/patches/v7_1/rename_scheduler_log_to_error_log.py new file mode 100644 index 0000000000..fdc6b0a44f --- /dev/null +++ b/frappe/patches/v7_1/rename_scheduler_log_to_error_log.py @@ -0,0 +1,10 @@ +import frappe + +def execute(): + if not 'tabError Log' in frappe.db.get_tables(): + frappe.rename_doc('DocType', 'Scheduler Log', 'Error Log') + frappe.db.sql("""delete from `tabError Log` where datediff(curdate(), creation) > 30""") + frappe.db.commit() + frappe.db.sql('alter table `tabError Log` change column name name varchar(140)') + frappe.db.sql('alter table `tabError Log` change column parent parent varchar(140)') + frappe.db.sql('alter table `tabError Log` engine=MyISAM') diff --git a/frappe/patches/v7_1/setup_integration_services.py b/frappe/patches/v7_1/setup_integration_services.py new file mode 100644 index 0000000000..7966b05c95 --- /dev/null +++ b/frappe/patches/v7_1/setup_integration_services.py @@ -0,0 +1,104 @@ +from __future__ import unicode_literals +import frappe +from frappe.exceptions import DataError +from frappe.utils.password import get_decrypted_password +import json + +app_list = [ + {"app_name": "razorpay_integration", "service_name": "Razorpay", "doctype": "Razorpay Settings", "remove": True}, + {"app_name": "paypal_integration", "service_name": "PayPal", "doctype": "PayPal Settings", "remove": True}, + {"app_name": "frappe", "service_name": "Dropbox Integration", "doctype": "Dropbox Backup", "remove": False} +] + +def execute(): + frappe.reload_doc("integration_broker", "doctype", "integration_service") + + installed_apps = frappe.get_installed_apps() + + for app_details in app_list: + if app_details["app_name"] in installed_apps: + try: + setup_integration_service(app_details) + + except DataError: + pass + + + if app_details["remove"]: + uninstall_app(app_details["app_name"]) + + frappe.delete_doc("DocType", "Dropbox Backup") + +def setup_integration_service(app_details): + settings = get_app_settings(app_details) + + if not settings: + raise DataError + + if frappe.db.exists("Integration Service", app_details["service_name"]): + integration_service = frappe.get_doc("Integration Service", app_details["service_name"]) + else: + integration_service = frappe.new_doc("Integration Service") + integration_service.service = app_details["service_name"] + + integration_service.enabled = 1 + integration_service.custom_settings_json = json.dumps(settings) if settings else '' + integration_service.flags.ignore_mandatory = True + integration_service.save(ignore_permissions=True) + +def get_app_settings(app_details): + from frappe.integration_broker.doctype.integration_service.integration_service import get_integration_controller + + parameters = {} + doctype = docname = app_details["doctype"] + + app_settings = get_parameters(app_details) + settings = app_settings["settings"] + + controller = get_integration_controller(app_details["service_name"]) + + for d in controller.parameters_template: + if settings.get(d.fieldname): + if ''.join(set(settings.get(d.fieldname))) == '*': + setattr(settings, d.fieldname, get_decrypted_password(doctype, docname, d.fieldname, raise_exception=True)) + + parameters.update({d.fieldname : settings.get(d.fieldname)}) + + return parameters + +def uninstall_app(app_name): + from frappe.installer import remove_from_installed_apps + remove_from_installed_apps(app_name) + +def get_parameters(app_details): + if app_details["service_name"] == "Razorpay": + return {"settings": frappe.get_doc(app_details["doctype"])} + + elif app_details["service_name"] == "PayPal": + if frappe.conf.paypal_username and frappe.conf.paypal_password and frappe.conf.paypal_signature: + return { + "settings": { + "api_username": frappe.conf.paypal_username, + "api_password": frappe.conf.paypal_password, + "signature": frappe.conf.paypal_signature + } + } + else: + return {"settings": frappe.get_doc(app_details["doctype"])} + + elif app_details["service_name"] == "Dropbox Integration": + doc = frappe.db.get_value(app_details["doctype"], None, + ["dropbox_access_key", "dropbox_access_secret", "upload_backups_to_dropbox"], as_dict=1) + + if not (frappe.conf.dropbox_access_key and frappe.conf.dropbox_secret_key): + raise DataError + + return { + "settings": { + "app_access_key": frappe.conf.dropbox_access_key, + "app_secret_key": frappe.conf.dropbox_secret_key, + "dropbox_access_key": doc.dropbox_access_key, + "dropbox_access_secret": doc.dropbox_access_secret, + "backup_frequency": doc.upload_backups_to_dropbox + } + } diff --git a/frappe/patches/v7_1/sync_language_doctype.py b/frappe/patches/v7_1/sync_language_doctype.py new file mode 100644 index 0000000000..a3eda183b2 --- /dev/null +++ b/frappe/patches/v7_1/sync_language_doctype.py @@ -0,0 +1,21 @@ +import frappe +from frappe.translate import get_lang_dict + +def execute(): + frappe.reload_doc('core', 'doctype', 'language') + + from frappe.core.doctype.language.language import sync_languages + sync_languages() + + # move language from old style to new style for old accounts + # i.e. from "english" to "en" + + lang_dict = get_lang_dict() + language = frappe.db.get_value('System Settings', None, 'language') + if language: + frappe.db.set_value('System Settings', None, 'language', lang_dict.get('language') or 'en') + + for user in frappe.get_all('User', fields=['name', 'language']): + if user.language: + frappe.db.set_value('User', user.name, 'language', + lang_dict.get('language') or 'en', update_modified=False) diff --git a/frappe/permissions.py b/frappe/permissions.py index 3f3727f955..e76603193b 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -303,7 +303,7 @@ def has_controller_permissions(doc, ptype, user=None): def can_set_user_permissions(doctype, docname=None): # System Manager can always set user permissions - if "System Manager" in frappe.get_roles(): + if frappe.session.user == "Administrator" or "System Manager" in frappe.get_roles(): return True meta = frappe.get_meta(doctype) diff --git a/frappe/print/doctype/print_format/print_format.js b/frappe/print/doctype/print_format/print_format.js index e6a99f146f..aba08102c7 100644 --- a/frappe/print/doctype/print_format/print_format.js +++ b/frappe/print/doctype/print_format/print_format.js @@ -15,6 +15,14 @@ frappe.ui.form.on("Print Format", "refresh", function(frm) { } if(!frm.is_new()) { + frm.add_custom_button(__("Edit Format"), function() { + if(!frm.doc.doc_type) { + msgprint(__("Please select DocType first")); + return; + } + frappe.set_route("print-format-builder", frm.doc.name); + }); + frm.add_custom_button(__("Make Default"), function() { frappe.call({ method: "frappe.print.doctype.print_format.print_format.make_default", @@ -22,17 +30,6 @@ frappe.ui.form.on("Print Format", "refresh", function(frm) { name: frm.doc.name } }) - }) - } -}); - -frappe.ui.form.on("Print Format", "edit_format", function(frm) { - if(!frm.doc.doc_type) { - msgprint(__("Please select DocType first")); - return; + }); } - frappe.route_options = { - print_format: frm - }; - frappe.set_route("print-format-builder"); }); diff --git a/frappe/print/doctype/print_format/print_format.json b/frappe/print/doctype/print_format/print_format.json index f4338d525a..a8e202f71e 100644 --- a/frappe/print/doctype/print_format/print_format.json +++ b/frappe/print/doctype/print_format/print_format.json @@ -8,11 +8,14 @@ "custom": 0, "docstatus": 0, "doctype": "DocType", + "editable_grid": 0, + "engine": "InnoDB", "fields": [ { "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "description": "", "fieldname": "doc_type", "fieldtype": "Link", @@ -39,6 +42,34 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, + "fieldname": "module", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Module", + "length": 0, + "no_copy": 0, + "options": "Module Def", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, "fieldname": "disabled", "fieldtype": "Check", "hidden": 0, @@ -63,6 +94,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "column_break_3", "fieldtype": "Column Break", "hidden": 0, @@ -86,6 +118,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "No", "fieldname": "standard", "fieldtype": "Select", @@ -114,6 +147,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "custom_format", "fieldtype": "Check", "hidden": 0, @@ -139,6 +173,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "custom_format", "fieldname": "section_break_6", "fieldtype": "Section Break", @@ -163,6 +198,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "Server", "depends_on": "custom_format", "description": "", @@ -191,6 +227,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "custom_format", "fieldname": "html", "fieldtype": "Code", @@ -219,6 +256,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "eval:!doc.custom_format", "fieldname": "section_break_9", "fieldtype": "Section Break", @@ -227,6 +265,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 0, + "label": "Style Settings", "length": 0, "no_copy": 0, "permlevel": 0, @@ -244,15 +283,70 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "depends_on": "eval:!doc.custom_format", - "fieldname": "edit_format", - "fieldtype": "Button", + "columns": 0, + "default": "1", + "fieldname": "align_labels_left", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Align Labels to the Left", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "1", + "fieldname": "show_section_headings", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Show Section Headings", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "1", + "fieldname": "line_breaks", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 0, - "label": "Edit Format", + "label": "Show Line Breaks after Sections", "length": 0, "no_copy": 0, "permlevel": 0, @@ -270,6 +364,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "column_break_11", "fieldtype": "Column Break", "hidden": 0, @@ -294,6 +389,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "default": "Default", "depends_on": "eval:!doc.custom_format", "fieldname": "font", @@ -322,6 +418,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "css_section", "fieldtype": "Section Break", "hidden": 0, @@ -346,6 +443,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "css", "fieldtype": "Code", "hidden": 0, @@ -371,6 +469,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "custom_html_help", "fieldtype": "HTML", "hidden": 0, @@ -397,6 +496,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "custom_format", "fieldname": "section_break_13", "fieldtype": "Section Break", @@ -422,6 +522,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "depends_on": "custom_format", "fieldname": "print_format_help", "fieldtype": "HTML", @@ -448,6 +549,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "format_data", "fieldtype": "Code", "hidden": 1, @@ -473,6 +575,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "print_format_builder", "fieldtype": "Check", "hidden": 1, @@ -499,13 +602,14 @@ "hide_toolbar": 0, "icon": "icon-print", "idx": 1, + "image_view": 0, "in_create": 0, "in_dialog": 0, "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-05-05 17:15:45.522931", + "modified": "2016-10-10 06:06:34.530193", "modified_by": "Administrator", "module": "Print", "name": "Print Format", @@ -521,6 +625,7 @@ "export": 0, "if_owner": 0, "import": 0, + "is_custom": 0, "permlevel": 0, "print": 1, "read": 1, diff --git a/frappe/print/doctype/print_format/print_format.py b/frappe/print/doctype/print_format/print_format.py index e205688630..2eb2af5285 100644 --- a/frappe/print/doctype/print_format/print_format.py +++ b/frappe/print/doctype/print_format/print_format.py @@ -23,6 +23,9 @@ class PrintFormat(Document): self.extract_images() + if not self.module: + self.module = frappe.db.get_value('DocType', self.doc_type, 'module') + if self.html: validate_template(self.html) @@ -45,11 +48,8 @@ class PrintFormat(Document): def export_doc(self): # export - if self.standard == 'Yes' and (frappe.conf.get('developer_mode') or 0) == 1: - module = frappe.db.get_value("DocType", self.doc_type, "module") - from frappe.modules.export_file import export_to_files - export_to_files(record_list=[['Print Format', self.name]], - record_module= module) + from frappe.modules.utils import export_module_json + export_module_json(self, self.standard == 'Yes', self.module) def on_trash(self): if self.doc_type: diff --git a/frappe/print/page/print_format_builder/print_format_builder.js b/frappe/print/page/print_format_builder/print_format_builder.js index 89d5b722bb..998f643f67 100644 --- a/frappe/print/page/print_format_builder/print_format_builder.js +++ b/frappe/print/page/print_format_builder/print_format_builder.js @@ -4,7 +4,13 @@ frappe.pages['print-format-builder'].on_page_load = function(wrapper) { } frappe.pages['print-format-builder'].on_page_show = function(wrapper) { - if(frappe.route_options) { + var route = frappe.get_route(); + if(route.length>1) { + frappe.model.with_doc('Print Format', route[1], function() { + frappe.print_format_builder.print_format = frappe.get_doc('Print Format', route[1]); + frappe.print_format_builder.refresh(); + }); + } else if(frappe.route_options) { if(frappe.route_options.make_new) { var doctype = frappe.route_options.doctype; var name = frappe.route_options.name; @@ -157,9 +163,10 @@ frappe.PrintFormatBuilder = Class.extend({ me.print_format = null; me.refresh(); }, true); - me.page.add_menu_item(__("Edit Properties"), function() { + me.page.clear_inner_toolbar(); + me.page.add_inner_button(__("Edit Properties"), function() { frappe.set_route("Form", "Print Format", me.print_format.name); - }, true); + }); }); }, setup_sidebar: function() { @@ -191,6 +198,7 @@ frappe.PrintFormatBuilder = Class.extend({ this.setup_sortable(); this.setup_add_section(); this.setup_edit_heading(); + this.setup_field_settings(); }, prepare_data: function() { this.print_heading_template = null; @@ -217,8 +225,9 @@ frappe.PrintFormatBuilder = Class.extend({ section.no_of_columns += 1; } - var set_section = function() { + var set_section = function(label) { section = me.get_new_section(); + if(label) section.label = label; column = null; me.layout_data.push(section); } @@ -239,7 +248,7 @@ frappe.PrintFormatBuilder = Class.extend({ } if(f.fieldtype==="Section Break") { - set_section(); + set_section(f.label); } else if(f.fieldtype==="Column Break") { set_column(); @@ -265,7 +274,7 @@ frappe.PrintFormatBuilder = Class.extend({ }); }, get_new_section: function() { - return {columns: [], no_of_columns: 0}; + return {columns: [], no_of_columns: 0, label:''}; }, get_new_column: function() { return {fields: []} @@ -296,7 +305,9 @@ frappe.PrintFormatBuilder = Class.extend({ // drag from fields library Sortable.create(this.page.sidebar.find(".print-format-builder-fields").get(0), { - group: {put: true, pull:"clone"}, + group: { + name:'field', put: true, pull:"clone" + }, sort: false, onAdd: function(evt) { // on drop, trash! @@ -318,11 +329,13 @@ frappe.PrintFormatBuilder = Class.extend({ var me = this; Sortable.create(col, { group: { + name: 'field', put: true, pull: true }, onAdd: function(evt) { // on drop, change the HTML + var $item = $(evt.item); if(!$item.hasClass("print-format-builder-field")) { var fieldname = $item.attr("data-fieldname"); @@ -330,11 +343,14 @@ frappe.PrintFormatBuilder = Class.extend({ if(fieldname==="_custom_html") { var field = me.get_custom_html_field(); } else { - var field = frappe.meta.get_docfield(me.print_format.doc_type, fieldname); + var field = frappe.meta.get_docfield(me.print_format.doc_type, + fieldname); } - $item.replaceWith(frappe.render_template("print_format_builder_field", - {field: field, me:me})) + var html = frappe.render_template("print_format_builder_field", + {field: field, me:me}); + + $item.replaceWith(html); } } }); @@ -355,6 +371,7 @@ frappe.PrintFormatBuilder = Class.extend({ this.page.main.on("click", ".section-settings", function() { var section = $(this).parent().parent(); var no_of_columns = section.find(".section-column").length; + var label = section.attr('data-label'); // new dialog var d = new frappe.ui.Dialog({ @@ -364,7 +381,13 @@ frappe.PrintFormatBuilder = Class.extend({ label:__("No of Columns"), fieldname:"no_of_columns", fieldtype:"Select", - options: ["1", "2", "3"], + options: ["1", "2", "3", "4"], + }, + { + label:__("Section Heading"), + fieldname:"label", + fieldtype:"Data", + description: __('Will only be shown if section headings are enabled') }, { label: __("Remove Section"), @@ -383,15 +406,63 @@ frappe.PrintFormatBuilder = Class.extend({ }); d.set_input("no_of_columns", no_of_columns + ""); + d.set_input("label", label || ""); d.set_primary_action(__("Update"), function() { // resize number of columns me.update_columns_in_section(section, no_of_columns, cint(d.get_value("no_of_columns"))); + section.attr('data-label', d.get_value('label') || ''); + section.find('.section-label').html(d.get_value('label') || ''); + + d.hide(); + }); + + d.show(); + + return false; + }); + }, + setup_field_settings: function() { + var me = this; + this.page.main.on("click", ".field-settings", function() { + var field = $(this).parent(); + + // new dialog + var d = new frappe.ui.Dialog({ + title: "Set Properties", + fields: [ + { + label:__("Label"), + fieldname:"label", + fieldtype:"Data" + }, + { + label: __("Align Value"), + fieldname: "align", + fieldtype: "Select", + options: [{'label': __('Left'), 'value': 'left'}, {'label': __('Right'), 'value': 'right'}] + }, + ], + }); + + d.set_value('label', field.attr("data-label")); + + d.set_primary_action(__("Update"), function() { + field.attr('data-align', d.get_value('align')); + field.attr('data-label', d.get_value('label')); + field.find('.field-label').html(d.get_value('label')); d.hide(); }); + // set current value + if(field.attr('data-align')) { + d.set_value('align', field.attr('data-align')); + } else { + d.set_value('align', 'left'); + } + d.show(); return false; @@ -457,6 +528,9 @@ frappe.PrintFormatBuilder = Class.extend({ }, setup_edit_heading: function() { var me = this; + if (!me.print_heading_template) { + $(this.page.main.find(".print-heading")).html('

    '+me.print_format.doc_type+'
    {{ doc.name }}

    ') + } this.page.main.find(".edit-heading").on("click", function() { var $heading = $(this).parents(".print-format-builder-header:first") .find(".print-format-builder-print-heading"); @@ -483,16 +557,16 @@ frappe.PrintFormatBuilder = Class.extend({ }); var $body = $(d.body); - - + + var doc_fields = frappe.get_meta(doctype).fields; var docfields_by_name = {}; - + // docfields by fieldname $.each(doc_fields, function(j, f) { if(f) docfields_by_name[f.fieldname] = f; }) - + // add field which are in column_names first to preserve order var fields = []; $.each(column_names, function(i, v) { @@ -502,7 +576,7 @@ frappe.PrintFormatBuilder = Class.extend({ }) // add remaining fields $.each(doc_fields, function(j, f) { - if (f && !in_list(column_names, f.fieldname) + if (f && !in_list(column_names, f.fieldname) && !in_list(["Section Break", "Column Break"], f.fieldtype) && f.label) { fields.push(f); } @@ -609,16 +683,28 @@ frappe.PrintFormatBuilder = Class.extend({ // add pages this.page.main.find(".print-format-builder-section").each(function() { - data.push({"fieldtype": "Section Break"}); + var section = {"fieldtype": "Section Break", 'label': $(this).attr('data-label') || '' }; + data.push(section); $(this).find(".print-format-builder-column").each(function() { data.push({"fieldtype": "Column Break"}); $(this).find(".print-format-builder-field").each(function() { var $this = $(this), fieldtype = $this.attr("data-fieldtype"), + align = $this.attr('data-align'), + label = $this.attr('data-label'), df = { fieldname: $this.attr("data-fieldname"), print_hide: 0 }; + + if(align) { + df.align = align; + } + + if(label) { + df.label = label; + } + if(fieldtype==="Table") { // append the user selected columns to visible_columns var columns = $this.attr("data-columns").split(","); @@ -653,7 +739,7 @@ frappe.PrintFormatBuilder = Class.extend({ }, callback: function(r) { me.print_format = r.message; - msgprint(__("Saved")); + frappe.show_alert({message: __("Saved"), indicator: 'green'}); } }); } diff --git a/frappe/print/page/print_format_builder/print_format_builder_field.html b/frappe/print/page/print_format_builder/print_format_builder_field.html index 299cfa2a39..67988bf842 100644 --- a/frappe/print/page/print_format_builder/print_format_builder_field.html +++ b/frappe/print/page/print_format_builder/print_format_builder_field.html @@ -2,12 +2,17 @@ {% if(field.print_hide) { %}style="background-color: #F7FAFC; color: #8D99A6;" title="{{ __("Hidden") }}"{% } %} data-fieldname="{%= field.fieldname %}" - data-label="{%= field.label %}" + data-label="{{ field.label }}" + + {% if field.align %}data-align="{{ field.align }}"{% endif %} data-fieldtype="{%= field.fieldtype %}" {% if(field.fieldtype==="Table") { %} data-columns="{%= me.get_visible_columns_string(field) %}" data-doctype="{%= field.options %}" {% } %}> + + {% if(field.fieldtype==="Custom HTML") { %}
    {%= field.options || me.get_no_content() %}
    {% } else { %} - {%= field.label %} + {{ field.label }} {% if(field.fieldtype==="Table") { %} ({%= __("Table") %})
    diff --git a/frappe/print/page/print_format_builder/print_format_builder_layout.html b/frappe/print/page/print_format_builder/print_format_builder_layout.html index 713bf1643a..0d9c9293a7 100644 --- a/frappe/print/page/print_format_builder/print_format_builder_layout.html +++ b/frappe/print/page/print_format_builder/print_format_builder_layout.html @@ -8,8 +8,11 @@ {%= __("Edit Heading") %}